feat: Orthographic 카메라 추가 — 실측 확인용

사용자 피드백: 거더 높이 변경 시 차이가 미미해 보이는 것은 원근 투영의
시각 효과일 수 있음. 평행(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 <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-15 10:20:59 +09:00
parent e986604f49
commit 2cb549fd9f
2 changed files with 56 additions and 2 deletions

View File

@@ -32,6 +32,16 @@ pub enum StandardView {
Iso, // Home 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. /// Orbit camera — spherical coordinates around a fixed target point.
/// ///
/// All distances in millimetres (scene units). /// All distances in millimetres (scene units).
@@ -48,6 +58,8 @@ pub struct Camera {
pub aspect: f32, pub aspect: f32,
pub znear: f32, pub znear: f32,
pub zfar: f32, pub zfar: f32,
/// 투영 모드 (원근 / 평행). 토글 O.
pub projection: Projection,
} }
impl Camera { impl Camera {
@@ -62,6 +74,7 @@ impl Camera {
aspect: 16.0 / 9.0, aspect: 16.0 / 9.0,
znear: 10.0, // 10 mm znear: 10.0, // 10 mm
zfar: 10_000_000.0, // 10 km zfar: 10_000_000.0, // 10 km
projection: Projection::Perspective,
} }
} }
@@ -81,10 +94,29 @@ impl Camera {
/// View-projection matrix (right-handed, depth 0→1). /// View-projection matrix (right-handed, depth 0→1).
pub fn view_proj(&self) -> Mat4 { pub fn view_proj(&self) -> Mat4 {
let view = Mat4::look_at_rh(self.eye(), self.target, Vec3::Y); 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 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. /// Build GPU uniform from current state.
pub fn to_uniform(&self) -> CameraUniform { pub fn to_uniform(&self) -> CameraUniform {
CameraUniform { view_proj: self.view_proj().to_cols_array_2d() } CameraUniform { view_proj: self.view_proj().to_cols_array_2d() }

View File

@@ -23,7 +23,7 @@ use wgpu::util::DeviceExt;
use cimery_kernel::OcctKernel; use cimery_kernel::OcctKernel;
#[cfg(not(feature = "occt"))] #[cfg(not(feature = "occt"))]
use cimery_kernel::PureRustKernel; use cimery_kernel::PureRustKernel;
use camera::{Camera, StandardView}; use camera::{Camera, Projection, StandardView};
use glam; use glam;
use bridge_scene::{ use bridge_scene::{
GirderSectionType, SceneParams, GirderSectionType, SceneParams,
@@ -277,6 +277,7 @@ impl RenderState {
aspect: 16.0 / 9.0, aspect: 16.0 / 9.0,
znear: 10.0, znear: 10.0,
zfar: 10_000_000.0, zfar: 10_000_000.0,
projection: Projection::Perspective,
}; };
let _ = mesh.aabb(); // keep aabb call for future use let _ = mesh.aabb(); // keep aabb call for future use
camera.resize(surface_config.width, surface_config.height); camera.resize(surface_config.width, surface_config.height);
@@ -562,6 +563,9 @@ impl RenderState {
let mut dirty = self.dirty; let mut dirty = self.dirty;
let was_dirty = dirty; let was_dirty = dirty;
let mut apply = false; 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) // Sprint 17: alignment display info (capture before closure)
let state_alignment_name: Option<String> = self.alignment_scene.alignment let state_alignment_name: Option<String> = self.alignment_scene.alignment
.as_ref().map(|a| a.name.clone()); .as_ref().map(|a| a.name.clone());
@@ -662,6 +666,15 @@ impl RenderState {
let prev_al = p.show_alignment; let prev_al = p.show_alignment;
ui.checkbox(&mut p.show_alignment, "선형 표시"); ui.checkbox(&mut p.show_alignment, "선형 표시");
if prev_al != p.show_alignment { dirty = true; } 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(); ui.separator();
@@ -757,6 +770,10 @@ impl RenderState {
} }
} }
if apply { self.rebuild_mesh(); } if apply { self.rebuild_mesh(); }
if toggle_ortho {
self.camera.toggle_projection();
self.update_camera();
}
// ── 3D scene ───────────────────────────────────────────────────────── // ── 3D scene ─────────────────────────────────────────────────────────
let output = self.surface.get_current_texture()?; 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.camera.zoom_extents(state.scene_mn, state.scene_mx);
state.update_camera(); state.update_camera();
} }
// O → Perspective ↔ Orthographic 투영 토글 (실측 확인용)
PhysicalKey::Code(KeyCode::KeyO) => {
state.camera.toggle_projection();
state.update_camera();
}
// Numpad 7 / 7 → Top view // Numpad 7 / 7 → Top view
PhysicalKey::Code(KeyCode::Numpad7) | PhysicalKey::Code(KeyCode::Digit7) => { PhysicalKey::Code(KeyCode::Numpad7) | PhysicalKey::Code(KeyCode::Digit7) => {
state.camera.set_standard_view(StandardView::Top); state.camera.set_standard_view(StandardView::Top);