viewer: Pan + ZoomExtents + 표준 뷰 단축키
camera.rs: - pan(dx, dy): Shift+중간버튼으로 target 이동 (화면 평면 이동) - zoom_extents(mn, mx): E키로 전체 씬 맞춤 - set_standard_view(StandardView): 축 고정 뷰 - StandardView: Top(7) / Front(1) / Right(3) / Left(4) / Iso(Home) lib.rs: - shift_pressed 추적 (ModifiersChanged) - Shift+중간버튼 드래그 → pan - E → ZoomExtents - 1/3/4/7/Home → 표준 뷰 스냅 - scene_mn/mx 저장 → zoom_extents에 전달 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,16 @@ impl CameraUniform {
|
|||||||
|
|
||||||
// ─── Camera ──────────────────────────────────────────────────────────────────
|
// ─── 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.
|
/// Orbit camera — spherical coordinates around a fixed target point.
|
||||||
///
|
///
|
||||||
/// All distances in millimetres (scene units).
|
/// All distances in millimetres (scene units).
|
||||||
@@ -94,6 +104,42 @@ impl Camera {
|
|||||||
self.radius = (self.radius * (1.0 - delta * 0.1)).max(100.0);
|
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.
|
/// Update aspect ratio on window resize.
|
||||||
pub fn resize(&mut self, width: u32, height: u32) {
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
self.aspect = width as f32 / height.max(1) as f32;
|
self.aspect = width as f32 / height.max(1) as f32;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use cimery_kernel::OcctKernel;
|
|||||||
#[cfg(not(feature = "occt"))]
|
#[cfg(not(feature = "occt"))]
|
||||||
use cimery_kernel::PureRustKernel;
|
use cimery_kernel::PureRustKernel;
|
||||||
use cimery_kernel::GeomKernel;
|
use cimery_kernel::GeomKernel;
|
||||||
use camera::Camera;
|
use camera::{Camera, StandardView};
|
||||||
use glam;
|
use glam;
|
||||||
|
|
||||||
// ─── Vertex ───────────────────────────────────────────────────────────────────
|
// ─── Vertex ───────────────────────────────────────────────────────────────────
|
||||||
@@ -80,9 +80,13 @@ struct RenderState {
|
|||||||
camera_bind_group: wgpu::BindGroup,
|
camera_bind_group: wgpu::BindGroup,
|
||||||
// Depth
|
// Depth
|
||||||
depth_view: wgpu::TextureView,
|
depth_view: wgpu::TextureView,
|
||||||
// Mouse state
|
// Mouse / keyboard state
|
||||||
mid_pressed: bool,
|
mid_pressed: bool,
|
||||||
|
shift_pressed: bool,
|
||||||
last_mouse: winit::dpi::PhysicalPosition<f64>,
|
last_mouse: winit::dpi::PhysicalPosition<f64>,
|
||||||
|
// Scene extents for ZoomExtents
|
||||||
|
scene_mn: [f32; 3],
|
||||||
|
scene_mx: [f32; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderState {
|
impl RenderState {
|
||||||
@@ -271,6 +275,8 @@ impl RenderState {
|
|||||||
cache: None,
|
cache: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let (scene_mn, scene_mx) = bridge_scene::scene_extents();
|
||||||
|
|
||||||
RenderState {
|
RenderState {
|
||||||
window,
|
window,
|
||||||
device,
|
device,
|
||||||
@@ -286,7 +292,10 @@ impl RenderState {
|
|||||||
camera_bind_group,
|
camera_bind_group,
|
||||||
depth_view,
|
depth_view,
|
||||||
mid_pressed: false,
|
mid_pressed: false,
|
||||||
|
shift_pressed: false,
|
||||||
last_mouse: winit::dpi::PhysicalPosition { x: 0.0, y: 0.0 },
|
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 {
|
match event {
|
||||||
// ── Exit ──────────────────────────────────────────────────────────
|
// ── Exit ──────────────────────────────────────────────────────────
|
||||||
WindowEvent::CloseRequested => event_loop.exit(),
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
WindowEvent::KeyboardInput {
|
|
||||||
event: KeyEvent {
|
// ── Shift modifier tracking ───────────────────────────────────────
|
||||||
physical_key: PhysicalKey::Code(KeyCode::Escape), ..
|
WindowEvent::ModifiersChanged(mods) => {
|
||||||
}, ..
|
state.shift_pressed = mods.state().shift_key();
|
||||||
} => event_loop.exit(),
|
}
|
||||||
|
|
||||||
|
// ── 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 ────────────────────────────────────────────────────────
|
// ── Resize ────────────────────────────────────────────────────────
|
||||||
WindowEvent::Resized(sz) => state.resize(sz),
|
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, .. } => {
|
WindowEvent::MouseInput { button: MouseButton::Middle, state: btn_state, .. } => {
|
||||||
state.mid_pressed = btn_state == ElementState::Pressed;
|
state.mid_pressed = btn_state == ElementState::Pressed;
|
||||||
}
|
}
|
||||||
@@ -433,7 +482,13 @@ impl ApplicationHandler for CimeryApp {
|
|||||||
if state.mid_pressed {
|
if state.mid_pressed {
|
||||||
let dx = (position.x - state.last_mouse.x) as f32;
|
let dx = (position.x - state.last_mouse.x) as f32;
|
||||||
let dy = (position.y - state.last_mouse.y) as f32;
|
let dy = (position.y - state.last_mouse.y) as f32;
|
||||||
|
if state.shift_pressed {
|
||||||
|
// Shift + Middle = Pan (translate target)
|
||||||
|
state.camera.pan(dx, dy);
|
||||||
|
} else {
|
||||||
|
// Middle = Orbit
|
||||||
state.camera.orbit(dx, dy);
|
state.camera.orbit(dx, dy);
|
||||||
|
}
|
||||||
state.update_camera();
|
state.update_camera();
|
||||||
}
|
}
|
||||||
state.last_mouse = position;
|
state.last_mouse = position;
|
||||||
|
|||||||
Reference in New Issue
Block a user