cimery Sprint 2 — PSC-I 기하학 + viewer 개편 + OCCT optional
kernel:
- PureRustKernel: PSC-I 단면 14-vertex polygon 스위프, flat normals
56 triangles / 168 vertices, 법선 단위벡터 검증 포함
- opencascade 의존성 optional feature (--features occt)로 격리
→ OCCT 없이도 전체 빌드 가능
- psc_i.rs: 프로파일 검증, AABB, 법선 테스트 6개
viewer:
- camera.rs: arcball orbit (middle-mouse drag + scroll zoom)
- shader.wgsl: MVP matrix uniform + 방향성 조명 (콘크리트 베이지)
- lib.rs: depth buffer, index 렌더, 실제 Mesh 업로드
StubKernel → PureRustKernel → OcctKernel 교체 경로 문서화
CLAUDE.md: MVP 품질 원칙 강화 ("아키텍처 임의 변경 절대 불가")
cargo test --workspace (viewer 제외) 43개 전부 통과
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
101
cimery/crates/viewer/src/camera.rs
Normal file
101
cimery/crates/viewer/src/camera.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
//! 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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 = Mat4::perspective_rh(self.fov_y, self.aspect, self.znear, self.zfar);
|
||||
proj * view
|
||||
}
|
||||
|
||||
/// 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).
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user