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:
117
crates/sylpheed-viewer/src/camera.rs
Normal file
117
crates/sylpheed-viewer/src/camera.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user