From 2cb549fd9ffb2a141226920357779d4c3ea48f03 Mon Sep 17 00:00:00 2001 From: minsung Date: Wed, 15 Apr 2026 10:20:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Orthographic=20=EC=B9=B4=EB=A9=94?= =?UTF-8?q?=EB=9D=BC=20=EC=B6=94=EA=B0=80=20=E2=80=94=20=EC=8B=A4=EC=B8=A1?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 피드백: 거더 높이 변경 시 차이가 미미해 보이는 것은 원근 투영의 시각 효과일 수 있음. 평행(Orthographic) 투영으로 보면 거리 무관 실측 크기가 그대로 보여 모델이 실제로 변하는지 객관적으로 확인 가능. 변경: - camera.rs: Projection enum (Perspective / Orthographic) 추가. - Camera.projection 필드 + view_proj() 분기. - Ortho 반높이 = radius * tan(fov_y/2) → 전환 시 시각 스케일 일치. - toggle_projection() 메서드. - lib.rs: - 카메라 초기값 projection: Perspective. - 키 O → 투영 토글. - egui 표시 섹션에 투영 버튼 추가 (◇ Perspective / ■ Ortho). 사용: 거더 높이 슬라이더 조정 → Apply → O 키로 Ortho 전환 → 모델 치수 실측 확인. Co-Authored-By: Claude Sonnet 4.6 --- cimery/crates/viewer/src/camera.rs | 34 +++++++++++++++++++++++++++++- cimery/crates/viewer/src/lib.rs | 24 ++++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/cimery/crates/viewer/src/camera.rs b/cimery/crates/viewer/src/camera.rs index e6faf0d..dd976cb 100644 --- a/cimery/crates/viewer/src/camera.rs +++ b/cimery/crates/viewer/src/camera.rs @@ -32,6 +32,16 @@ pub enum StandardView { 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). @@ -48,6 +58,8 @@ pub struct Camera { pub aspect: f32, pub znear: f32, pub zfar: f32, + /// 투영 모드 (원근 / 평행). 토글 O. + pub projection: Projection, } impl Camera { @@ -62,6 +74,7 @@ impl Camera { aspect: 16.0 / 9.0, znear: 10.0, // 10 mm zfar: 10_000_000.0, // 10 km + projection: Projection::Perspective, } } @@ -81,10 +94,29 @@ impl Camera { /// 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 = Mat4::perspective_rh(self.fov_y, self.aspect, self.znear, self.zfar); + 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() } diff --git a/cimery/crates/viewer/src/lib.rs b/cimery/crates/viewer/src/lib.rs index d0fd7ea..c833f74 100644 --- a/cimery/crates/viewer/src/lib.rs +++ b/cimery/crates/viewer/src/lib.rs @@ -23,7 +23,7 @@ use wgpu::util::DeviceExt; use cimery_kernel::OcctKernel; #[cfg(not(feature = "occt"))] use cimery_kernel::PureRustKernel; -use camera::{Camera, StandardView}; +use camera::{Camera, Projection, StandardView}; use glam; use bridge_scene::{ GirderSectionType, SceneParams, @@ -277,6 +277,7 @@ impl RenderState { aspect: 16.0 / 9.0, znear: 10.0, zfar: 10_000_000.0, + projection: Projection::Perspective, }; let _ = mesh.aabb(); // keep aabb call for future use camera.resize(surface_config.width, surface_config.height); @@ -562,6 +563,9 @@ impl RenderState { let mut dirty = self.dirty; let was_dirty = dirty; let mut apply = false; + // Projection toggle: egui 버튼이 self.camera 를 직접 못 건드리므로 플래그로 전달. + let mut toggle_ortho = false; + let is_ortho_now = self.camera.projection == Projection::Orthographic; // Sprint 17: alignment display info (capture before closure) let state_alignment_name: Option = self.alignment_scene.alignment .as_ref().map(|a| a.name.clone()); @@ -662,6 +666,15 @@ impl RenderState { let prev_al = p.show_alignment; ui.checkbox(&mut p.show_alignment, "선형 표시"); if prev_al != p.show_alignment { dirty = true; } + + // 투영 모드 토글 (dirty 와 무관, 즉시 적용) + ui.horizontal(|ui| { + ui.label("투영:"); + let label = if is_ortho_now { "■ Ortho (O)" } else { "◇ Perspective (O)" }; + if ui.button(label).clicked() { + toggle_ortho = true; + } + }); }); ui.separator(); @@ -757,6 +770,10 @@ impl RenderState { } } if apply { self.rebuild_mesh(); } + if toggle_ortho { + self.camera.toggle_projection(); + self.update_camera(); + } // ── 3D scene ───────────────────────────────────────────────────────── let output = self.surface.get_current_texture()?; @@ -913,6 +930,11 @@ impl ApplicationHandler for CimeryApp { state.camera.zoom_extents(state.scene_mn, state.scene_mx); state.update_camera(); } + // O → Perspective ↔ Orthographic 투영 토글 (실측 확인용) + PhysicalKey::Code(KeyCode::KeyO) => { + state.camera.toggle_projection(); + state.update_camera(); + } // Numpad 7 / 7 → Top view PhysicalKey::Code(KeyCode::Numpad7) | PhysicalKey::Code(KeyCode::Digit7) => { state.camera.set_standard_view(StandardView::Top);