feat: initialise workspace — Milestone 1 asset explorer

Three-crate Cargo workspace structured per PROJECT.md spec:
- crates/sylpheed-formats  — Xbox 360 format parsers (no Bevy)
- crates/sylpheed-viewer   — Bevy 0.15 asset viewer + egui UI
- crates/sylpheed-cli      — CLI tools (extract/list/sniff/texture)

Milestone 1 features:
- XISO disc image reading via xdvdfs 0.8
- XPR2 texture container parsing + Morton de-tiling
- D3DFORMAT → wgpu TextureFormat mapping (DXT1/3/5, DXN, ARGB)
- Custom Bevy AssetLoader for .xpr files
- Orbit camera (LMB orbit, RMB pan, scroll zoom)
- egui file browser + RE notes panel
- CLI: extract / list / sniff / texture info / texture export
- GitHub Actions CI (Linux, macOS, Windows, WASM)
- Trunk WASM build config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-03-25 21:04:07 +01:00
commit f8127e73b0
23 changed files with 8107 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
//! Orbit camera for inspecting 3D assets.
//!
//! Controls:
//! - Left-click + drag → orbit
//! - Right-click + drag → pan
//! - Scroll wheel → zoom
//! - R → reset to default view
use bevy::prelude::*;
use bevy::input::mouse::{MouseMotion, MouseWheel};
pub struct OrbitCameraPlugin;
impl Plugin for OrbitCameraPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, spawn_camera)
.add_systems(Update, orbit_camera);
}
}
/// State for the orbit camera controller.
#[derive(Component)]
pub struct OrbitCamera {
/// Distance from the focus point
pub radius: f32,
/// Rotation around the vertical axis (azimuth) in radians
pub yaw: f32,
/// Rotation around the horizontal axis (elevation) in radians
pub pitch: f32,
/// The point the camera orbits around
pub focus: Vec3,
pub orbit_sensitivity: f32,
pub zoom_sensitivity: f32,
pub pan_sensitivity: f32,
}
impl Default for OrbitCamera {
fn default() -> Self {
Self {
radius: 5.0,
yaw: std::f32::consts::FRAC_PI_4,
pitch: std::f32::consts::FRAC_PI_6,
focus: Vec3::ZERO,
orbit_sensitivity: 0.005,
zoom_sensitivity: 0.3,
pan_sensitivity: 0.003,
}
}
}
fn spawn_camera(mut commands: Commands) {
let orbit = OrbitCamera::default();
let transform = orbit_transform(&orbit);
commands.spawn((
Camera3d::default(),
transform,
orbit,
));
}
fn orbit_camera(
mut query: Query<(&mut OrbitCamera, &mut Transform)>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
keys: Res<ButtonInput<KeyCode>>,
mut mouse_motion: EventReader<MouseMotion>,
mut scroll: EventReader<MouseWheel>,
) {
let Ok((mut cam, mut transform)) = query.get_single_mut() else { return };
let mut delta_motion = Vec2::ZERO;
for ev in mouse_motion.read() {
delta_motion += ev.delta;
}
let mut scroll_delta = 0.0f32;
for ev in scroll.read() {
scroll_delta += ev.y;
}
// Orbit (left mouse drag)
if mouse_buttons.pressed(MouseButton::Left) {
cam.yaw -= delta_motion.x * cam.orbit_sensitivity;
cam.pitch -= delta_motion.y * cam.orbit_sensitivity;
// Clamp pitch to avoid gimbal lock
cam.pitch = cam.pitch.clamp(-1.5, 1.5);
}
// Pan (right mouse drag)
if mouse_buttons.pressed(MouseButton::Right) {
let right = transform.rotation * Vec3::X;
let up = transform.rotation * Vec3::Y;
// Copy fields before mutably borrowing `cam.focus`
let pan_sens = cam.pan_sensitivity;
let radius = cam.radius;
cam.focus -= right * delta_motion.x * pan_sens * radius;
cam.focus += up * delta_motion.y * pan_sens * radius;
}
// Zoom (scroll)
cam.radius -= scroll_delta * cam.zoom_sensitivity * cam.radius;
cam.radius = cam.radius.clamp(0.5, 50.0);
// Reset (R key)
if keys.just_pressed(KeyCode::KeyR) {
*cam = OrbitCamera::default();
}
*transform = orbit_transform(&cam);
}
fn orbit_transform(cam: &OrbitCamera) -> Transform {
let rotation = Quat::from_euler(EulerRot::YXZ, cam.yaw, cam.pitch, 0.0);
let offset = rotation * Vec3::new(0.0, 0.0, cam.radius);
Transform::from_translation(cam.focus + offset)
.looking_at(cam.focus, Vec3::Y)
}