diff --git a/cimery/crates/viewer/src/camera.rs b/cimery/crates/viewer/src/camera.rs index 93f04aa..e6faf0d 100644 --- a/cimery/crates/viewer/src/camera.rs +++ b/cimery/crates/viewer/src/camera.rs @@ -22,6 +22,16 @@ impl CameraUniform { // ─── Camera ────────────────────────────────────────────────────────────────── +/// Standard axis-aligned views (Revit numpad convention). +#[derive(Debug, Clone, Copy)] +pub enum StandardView { + Top, // Numpad 7 + Front, // Numpad 1 + Right, // Numpad 3 + Left, // Numpad 4 / Ctrl+Numpad3 + Iso, // Home +} + /// Orbit camera — spherical coordinates around a fixed target point. /// /// All distances in millimetres (scene units). @@ -94,6 +104,42 @@ impl Camera { self.radius = (self.radius * (1.0 - delta * 0.1)).max(100.0); } + /// Pan target by screen-space delta (Shift + middle mouse). + /// + /// Moves the orbit centre so the object stays under the cursor. + pub fn pan(&mut self, delta_x: f32, delta_y: f32) { + let eye = self.eye(); + let view_dir = (self.target - eye).normalize(); + let right = view_dir.cross(Vec3::Y).normalize(); + let up = right.cross(view_dir).normalize(); + let scale = self.radius * 0.001; // pan speed proportional to zoom + self.target -= right * delta_x * scale; + self.target += up * delta_y * scale; + } + + /// Zoom-extents: fit the given bounding box into view. + pub fn zoom_extents(&mut self, mn: [f32; 3], mx: [f32; 3]) { + let c = Vec3::new( + (mn[0] + mx[0]) * 0.5, + (mn[1] + mx[1]) * 0.5, + (mn[2] + mx[2]) * 0.5, + ); + let diag = Vec3::new(mx[0]-mn[0], mx[1]-mn[1], mx[2]-mn[2]).length(); + self.target = c; + self.radius = (diag * 0.75).max(1000.0); + } + + /// Snap to a standard axis-aligned view (Revit numpad convention). + pub fn set_standard_view(&mut self, view: StandardView) { + match view { + StandardView::Top => { self.yaw = 0.0; self.pitch = std::f32::consts::FRAC_PI_2 - 0.01; } + StandardView::Front => { self.yaw = std::f32::consts::PI; self.pitch = 0.0; } + StandardView::Right => { self.yaw = std::f32::consts::FRAC_PI_2; self.pitch = 0.0; } + StandardView::Left => { self.yaw = -std::f32::consts::FRAC_PI_2; self.pitch = 0.0; } + StandardView::Iso => { self.yaw = std::f32::consts::FRAC_PI_4; self.pitch = 0.35; } + } + } + /// Update aspect ratio on window resize. pub fn resize(&mut self, width: u32, height: u32) { self.aspect = width as f32 / height.max(1) as f32; diff --git a/cimery/crates/viewer/src/lib.rs b/cimery/crates/viewer/src/lib.rs index 2cee892..79ca973 100644 --- a/cimery/crates/viewer/src/lib.rs +++ b/cimery/crates/viewer/src/lib.rs @@ -31,7 +31,7 @@ use cimery_kernel::OcctKernel; #[cfg(not(feature = "occt"))] use cimery_kernel::PureRustKernel; use cimery_kernel::GeomKernel; -use camera::Camera; +use camera::{Camera, StandardView}; use glam; // ─── Vertex ─────────────────────────────────────────────────────────────────── @@ -80,9 +80,13 @@ struct RenderState { camera_bind_group: wgpu::BindGroup, // Depth depth_view: wgpu::TextureView, - // Mouse state - mid_pressed: bool, - last_mouse: winit::dpi::PhysicalPosition, + // Mouse / keyboard state + mid_pressed: bool, + shift_pressed: bool, + last_mouse: winit::dpi::PhysicalPosition, + // Scene extents for ZoomExtents + scene_mn: [f32; 3], + scene_mx: [f32; 3], } impl RenderState { @@ -271,6 +275,8 @@ impl RenderState { cache: None, }); + let (scene_mn, scene_mx) = bridge_scene::scene_extents(); + RenderState { window, device, @@ -285,8 +291,11 @@ impl RenderState { camera_buffer, camera_bind_group, depth_view, - mid_pressed: false, - last_mouse: winit::dpi::PhysicalPosition { x: 0.0, y: 0.0 }, + mid_pressed: false, + shift_pressed: false, + last_mouse: winit::dpi::PhysicalPosition { x: 0.0, y: 0.0 }, + scene_mn, + scene_mx, } } @@ -416,16 +425,56 @@ impl ApplicationHandler for CimeryApp { match event { // ── Exit ────────────────────────────────────────────────────────── WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { - event: KeyEvent { - physical_key: PhysicalKey::Code(KeyCode::Escape), .. - }, .. - } => event_loop.exit(), + + // ── Shift modifier tracking ─────────────────────────────────────── + WindowEvent::ModifiersChanged(mods) => { + state.shift_pressed = mods.state().shift_key(); + } + + // ── Keyboard shortcuts ──────────────────────────────────────────── + WindowEvent::KeyboardInput { event: KeyEvent { physical_key, state: key_state, .. }, .. } + if key_state == ElementState::Pressed => + { + match physical_key { + PhysicalKey::Code(KeyCode::Escape) => event_loop.exit(), + // E → ZoomExtents (fit all) + PhysicalKey::Code(KeyCode::KeyE) => { + state.camera.zoom_extents(state.scene_mn, state.scene_mx); + state.update_camera(); + } + // Numpad 7 / 7 → Top view + PhysicalKey::Code(KeyCode::Numpad7) | PhysicalKey::Code(KeyCode::Digit7) => { + state.camera.set_standard_view(StandardView::Top); + state.update_camera(); + } + // Numpad 1 / 1 → Front view + PhysicalKey::Code(KeyCode::Numpad1) | PhysicalKey::Code(KeyCode::Digit1) => { + state.camera.set_standard_view(StandardView::Front); + state.update_camera(); + } + // Numpad 3 / 3 → Right side view + PhysicalKey::Code(KeyCode::Numpad3) | PhysicalKey::Code(KeyCode::Digit3) => { + state.camera.set_standard_view(StandardView::Right); + state.update_camera(); + } + // Numpad 4 / 4 → Left side view + PhysicalKey::Code(KeyCode::Numpad4) | PhysicalKey::Code(KeyCode::Digit4) => { + state.camera.set_standard_view(StandardView::Left); + state.update_camera(); + } + // Home → Isometric (default view) + PhysicalKey::Code(KeyCode::Home) => { + state.camera.set_standard_view(StandardView::Iso); + state.update_camera(); + } + _ => {} + } + } // ── Resize ──────────────────────────────────────────────────────── WindowEvent::Resized(sz) => state.resize(sz), - // ── Mouse orbit (middle button drag) ────────────────────────────── + // ── Mouse orbit / pan (middle button drag) ──────────────────────── WindowEvent::MouseInput { button: MouseButton::Middle, state: btn_state, .. } => { state.mid_pressed = btn_state == ElementState::Pressed; } @@ -433,7 +482,13 @@ impl ApplicationHandler for CimeryApp { if state.mid_pressed { let dx = (position.x - state.last_mouse.x) as f32; let dy = (position.y - state.last_mouse.y) as f32; - state.camera.orbit(dx, dy); + if state.shift_pressed { + // Shift + Middle = Pan (translate target) + state.camera.pan(dx, dy); + } else { + // Middle = Orbit + state.camera.orbit(dx, dy); + } state.update_camera(); } state.last_mouse = position;