사용자 요청: 마우스 드래그 회전 방향이 직관과 반대라 반전. - yaw: delta_x * 0.005 부호 반전 (+= → -=) - pitch: delta_y * 0.005 부호 반전 (-= → +=) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
181 lines
6.9 KiB
Rust
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;
|
|
}
|
|
}
|