Files
ParaWiki/cimery/crates/viewer/src/camera.rs
minsung 750ac2247a fix: 카메라 회전 방향 반전
사용자 요청: 마우스 드래그 회전 방향이 직관과 반대라 반전.
- yaw:   delta_x * 0.005 부호 반전 (+= → -=)
- pitch: delta_y * 0.005 부호 반전 (-= → +=)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 12:04:12 +09:00

181 lines
6.9 KiB
Rust

//! Arcball/orbit camera — Revit ViewCube style.
//!
//! Orbit with middle-mouse drag, zoom with scroll wheel.
use glam::{Mat4, Vec3};
use bytemuck::{Pod, Zeroable};
// ─── GPU uniform ─────────────────────────────────────────────────────────────
/// 64-byte view-projection matrix uploaded to the GPU once per frame.
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct CameraUniform {
pub view_proj: [[f32; 4]; 4],
}
impl CameraUniform {
pub fn identity() -> Self {
Self { view_proj: Mat4::IDENTITY.to_cols_array_2d() }
}
}
// ─── 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
}
/// Camera projection mode.
///
/// - `Perspective`: 원근 투영. 멀수록 작게 보임. 일반 3D 뷰.
/// - `Orthographic`: 평행 투영. 거리 무관 실측 크기. 치수 검증·도면 스타일 뷰.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Projection {
Perspective,
Orthographic,
}
/// Orbit camera — spherical coordinates around a fixed target point.
///
/// All distances in millimetres (scene units).
pub struct Camera {
/// Point the camera orbits around.
pub target: Vec3,
/// Distance from target [mm].
pub radius: f32,
/// Horizontal rotation [radians].
pub yaw: f32,
/// Vertical rotation [radians]. Clamped to avoid gimbal lock.
pub pitch: f32,
pub fov_y: f32,
pub aspect: f32,
pub znear: f32,
pub zfar: f32,
/// 투영 모드 (원근 / 평행). 토글 O.
pub projection: Projection,
}
impl Camera {
/// Default view looking at a 40 m PSC-I girder from a comfortable angle.
pub fn default_for_girder(span_mm: f32) -> Self {
Self {
target: Vec3::new(300.0, 900.0, span_mm * 0.5),
radius: span_mm * 1.5,
yaw: std::f32::consts::FRAC_PI_4, // 45°
pitch: 0.35, // ~20°
fov_y: 60.0_f32.to_radians(),
aspect: 16.0 / 9.0,
znear: 10.0, // 10 mm
zfar: 10_000_000.0, // 10 km
projection: Projection::Perspective,
}
}
/// Eye position derived from orbit parameters.
pub fn eye(&self) -> Vec3 {
let sin_p = self.pitch.sin();
let cos_p = self.pitch.cos();
let sin_y = self.yaw.sin();
let cos_y = self.yaw.cos();
self.target + Vec3::new(
self.radius * cos_p * sin_y,
self.radius * sin_p,
self.radius * cos_p * cos_y,
)
}
/// View-projection matrix (right-handed, depth 0→1).
pub fn view_proj(&self) -> Mat4 {
let view = Mat4::look_at_rh(self.eye(), self.target, Vec3::Y);
let proj = match self.projection {
Projection::Perspective => {
Mat4::perspective_rh(self.fov_y, self.aspect, self.znear, self.zfar)
}
Projection::Orthographic => {
// Ortho 수직 반높이: 같은 radius·FOV 의 원근 뷰와 시각 스케일 일치 →
// target 평면에서 보이던 크기를 그대로 유지한 채 평행 투영.
let half_h = self.radius * (self.fov_y * 0.5).tan();
let half_w = half_h * self.aspect;
Mat4::orthographic_rh(-half_w, half_w, -half_h, half_h, self.znear, self.zfar)
}
};
proj * view
}
/// Perspective ↔ Orthographic 토글.
pub fn toggle_projection(&mut self) {
self.projection = match self.projection {
Projection::Perspective => Projection::Orthographic,
Projection::Orthographic => Projection::Perspective,
};
}
/// Build GPU uniform from current state.
pub fn to_uniform(&self) -> CameraUniform {
CameraUniform { view_proj: self.view_proj().to_cols_array_2d() }
}
// ── Interaction ────────────────────────────────────────────────────────
/// Orbit by dragging (delta in pixels, scaled to radians).
/// 사용자 요청: 회전 방향 반전 (yaw·pitch 양쪽 모두).
pub fn orbit(&mut self, delta_x: f32, delta_y: f32) {
self.yaw -= delta_x * 0.005;
self.pitch = (self.pitch + delta_y * 0.005)
.clamp(-std::f32::consts::FRAC_PI_2 + 0.05, std::f32::consts::FRAC_PI_2 - 0.05);
}
/// Zoom by scrolling (positive = closer, negative = farther).
pub fn zoom(&mut self, delta: f32) {
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;
}
}