품질 강화 — ADR-004 + IFC snapshot 테스트 + helper 유닛 + clippy 경고 정리
## ADR-004 (Output/reports/ADR-004-sprint-25-39-decisions.md) Sprint 25~39 기간의 **15개 아키텍처 결정** 정리: - D1~D9: 거더교 MVP 확장 (단면 분기·다경간·Skew 관례·방호벽·격벽·Camber·헌치·UI) - D10~D13: IFC4X3 Add2 익스포터 4 결정 (크레이트 분리·형상 전략 3단계·GUID·Camber 근사) - D14: proc-macro 스캐폴딩 (전면 #[param] 는 Feature 10+ 안정 후) - D15: 변단면 거더 알고리즘 (소핏 lift + Y 선형보간) - 미결 6항목 (Pset 확장·LinearPlacement·ElementAssembly·IfcPile·#[param] 전면·변단면 IFC) - 테스트 커버리지 101개 현황표 ## IFC 스냅샷 테스트 (crates/ifc/tests/snapshot_tests.rs) insta 기반 회귀 방지, 8개 baseline: - mask_guids(): 22자 IFC GUID 를 'GUID' 로 정규화 (결정적 비교 가능) - 시나리오: 기본 단경간 PSC-I / 2경간 π형 / skew 15° / camber 50mm / Rectangle 단면 / parapets off - mask_guids 자체 유닛 테스트 2개 ## Mesh helper 유닛 테스트 (crates/viewer/src/bridge_scene.rs helper_tests) 순수 함수 9개 검증: - apply_camber_mesh: zero 항등·midspan 도달값·경간 밖 미영향 - rotate_y_around_z: 0 회전 항등·90° 피봇 회전·정점 개수 보존 - apply_variable_depth: zero 항등·소핏 lift · 지점 0 lift ## clippy lib 경고 15+ → 0 - map_identity (kernel/expansion_joint.rs) - unnecessary_lazy_evaluations ×4 (dsl/abutment·pier·csv_template — auto-fix) - too_many_arguments (usd save_scene — allow with justification) - clamp-like 패턴 ×7 (viewer bridge_scene/incremental_scene 의 .max(1).min(N) → .clamp(1, N)) - redundant_closure ×2 (project_file 의 `|e| Error::other(e)` → `Error::other`) - redundant_guard ×1 (viewer KeyboardInput match guard → 패턴 내 직접 매치) cargo clippy --workspace --lib: 0 경고. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -228,10 +228,10 @@ fn merge(meshes: Vec<Mesh>) -> Mesh {
|
||||
pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<Mesh, KernelError> {
|
||||
let span_m = p.span_m;
|
||||
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||
let span_count = p.span_count.max(1).min(5);
|
||||
let span_count = p.span_count.clamp(1, 5);
|
||||
let total_m = span_m * span_count as f64;
|
||||
let total_mm = span_mm * span_count as f32;
|
||||
let n_girders = p.girder_count.max(1).min(10);
|
||||
let n_girders = p.girder_count.clamp(1, 10);
|
||||
let spacing = p.girder_spacing;
|
||||
let girder_h = p.girder_height;
|
||||
const BEARING_H: f32 = 60.0; // mm
|
||||
@@ -536,10 +536,10 @@ pub fn build_bridge_scene<K: GeomKernel>(kernel: &K, p: &SceneParams) -> Result<
|
||||
/// Sprint 26: 다경간 지원 — ground·alignment 가 전체 교량 길이 기준.
|
||||
pub fn build_background_scene(p: &SceneParams) -> Mesh {
|
||||
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||
let span_count = p.span_count.max(1).min(5);
|
||||
let span_count = p.span_count.clamp(1, 5);
|
||||
let total_mm = span_mm * span_count as f32;
|
||||
let girder_h = p.girder_height;
|
||||
let n_girders = p.girder_count.max(1).min(10);
|
||||
let n_girders = p.girder_count.clamp(1, 10);
|
||||
let spacing = p.girder_spacing;
|
||||
const BEARING_H: f32 = 60.0;
|
||||
let breast_wall_h = (girder_h + BEARING_H) as f64;
|
||||
@@ -580,7 +580,7 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
) -> Result<Vec<FeatureMesh>, KernelError> {
|
||||
let span_m = p.span_m;
|
||||
let span_mm = (p.span_m * 1_000.0) as f32;
|
||||
let n_girders = p.girder_count.max(1).min(10);
|
||||
let n_girders = p.girder_count.clamp(1, 10);
|
||||
let spacing = p.girder_spacing;
|
||||
let girder_h = p.girder_height;
|
||||
const BEARING_H: f32 = 60.0;
|
||||
@@ -616,7 +616,7 @@ pub fn build_selectable_scene<K: GeomKernel>(
|
||||
}
|
||||
};
|
||||
|
||||
let span_count = p.span_count.max(1).min(5);
|
||||
let span_count = p.span_count.clamp(1, 5);
|
||||
let total_m = span_m * span_count as f64;
|
||||
let total_mm = span_mm * span_count as f32;
|
||||
|
||||
@@ -877,3 +877,139 @@ pub fn scene_extents(p: &SceneParams) -> ([f32; 3], [f32; 3]) {
|
||||
let bot_y = -(p.girder_height + 3_000.0 + 1_000.0);
|
||||
([-half_w, bot_y, -2_000.0], [half_w, top_y, total_mm + 2_000.0])
|
||||
}
|
||||
|
||||
// ─── Mesh helper unit tests (Sprint 39 품질 강화) ─────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod helper_tests {
|
||||
use super::*;
|
||||
|
||||
fn unit_cube(z_offset: f32) -> Mesh {
|
||||
// 1×1×1 박스, Y 는 [0, 1], Z 는 [z_offset, z_offset+1].
|
||||
Mesh {
|
||||
vertices: vec![
|
||||
[0.0, 0.0, z_offset ],
|
||||
[1.0, 0.0, z_offset ],
|
||||
[1.0, 1.0, z_offset ],
|
||||
[0.0, 1.0, z_offset ],
|
||||
[0.0, 0.0, z_offset + 1.0 ],
|
||||
[1.0, 0.0, z_offset + 1.0 ],
|
||||
[1.0, 1.0, z_offset + 1.0 ],
|
||||
[0.0, 1.0, z_offset + 1.0 ],
|
||||
],
|
||||
indices: vec![],
|
||||
normals: vec![[0.0, 1.0, 0.0]; 8],
|
||||
colors: vec![[0.5; 3]; 8],
|
||||
}
|
||||
}
|
||||
|
||||
// ── apply_camber_mesh ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn camber_zero_does_nothing() {
|
||||
let mut m = unit_cube(0.0);
|
||||
let orig = m.vertices.clone();
|
||||
apply_camber_mesh(&mut m, 0.0, 1.0, 0.0);
|
||||
assert_eq!(m.vertices, orig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camber_at_midspan_reaches_mid_mm() {
|
||||
// span=10, mid_mm=100 → u=5 에서 lift = 4·100·5·5/100 = 100.
|
||||
let mut m = Mesh {
|
||||
vertices: vec![[0.0, 0.0, 5.0]],
|
||||
indices: vec![],
|
||||
normals: vec![[0.0, 1.0, 0.0]],
|
||||
colors: vec![[0.0; 3]],
|
||||
};
|
||||
apply_camber_mesh(&mut m, 0.0, 10.0, 100.0);
|
||||
assert!((m.vertices[0][1] - 100.0).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camber_outside_span_unaffected() {
|
||||
let mut m = Mesh {
|
||||
vertices: vec![[0.0, 0.0, -1.0], [0.0, 0.0, 20.0]],
|
||||
indices: vec![],
|
||||
normals: vec![[0.0, 1.0, 0.0]; 2],
|
||||
colors: vec![[0.0; 3]; 2],
|
||||
};
|
||||
apply_camber_mesh(&mut m, 0.0, 10.0, 100.0);
|
||||
assert_eq!(m.vertices[0][1], 0.0);
|
||||
assert_eq!(m.vertices[1][1], 0.0);
|
||||
}
|
||||
|
||||
// ── rotate_y_around_z ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn rotate_zero_angle_is_identity() {
|
||||
let m = unit_cube(5.0);
|
||||
let orig_v = m.vertices.clone();
|
||||
let rotated = rotate_y_around_z(m, 0.0, 5.0);
|
||||
assert_eq!(rotated.vertices, orig_v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_90_around_pivot_swaps_x_z() {
|
||||
// π/2 회전: pivot 기준, (x=1, z=pivot) → (x=0, z=pivot-1).
|
||||
let m = Mesh {
|
||||
vertices: vec![[1.0, 0.0, 5.0]],
|
||||
indices: vec![],
|
||||
normals: vec![[1.0, 0.0, 0.0]],
|
||||
colors: vec![[0.0; 3]],
|
||||
};
|
||||
let rotated = rotate_y_around_z(m, std::f32::consts::FRAC_PI_2, 5.0);
|
||||
assert!(rotated.vertices[0][0].abs() < 1e-5, "x should go to 0, got {}", rotated.vertices[0][0]);
|
||||
assert!((rotated.vertices[0][2] - 4.0).abs() < 1e-5, "z should go to 4, got {}", rotated.vertices[0][2]);
|
||||
// normals 도 회전: (1,0,0) → (0,0,-1).
|
||||
assert!((rotated.normals[0][2] + 1.0).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_preserves_vertex_count() {
|
||||
let m = unit_cube(0.0);
|
||||
let n = m.vertices.len();
|
||||
let rotated = rotate_y_around_z(m, 0.5, 0.0);
|
||||
assert_eq!(rotated.vertices.len(), n);
|
||||
}
|
||||
|
||||
// ── apply_variable_depth ──────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn variable_depth_zero_does_nothing() {
|
||||
let mut m = unit_cube(0.0);
|
||||
let orig = m.vertices.clone();
|
||||
apply_variable_depth(&mut m, 0.0, 1.0, 0.0, 1.0);
|
||||
assert_eq!(m.vertices, orig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_depth_lifts_soffit_more_than_top() {
|
||||
// girder_h=1, max=0.2, span=10, midspan u=5 → lift = 4·0.2·5·5/100 = 0.2.
|
||||
// y=0 vertex (소핏): +0.2 · (1 - 0/1) = +0.2 → Y=0.2.
|
||||
// y=1 vertex (상면): +0.2 · (1 - 1/1) = 0 → Y=1 (변화 없음).
|
||||
let mut m = Mesh {
|
||||
vertices: vec![[0.0, 0.0, 5.0], [0.0, 1.0, 5.0]],
|
||||
indices: vec![],
|
||||
normals: vec![[0.0, 1.0, 0.0]; 2],
|
||||
colors: vec![[0.0; 3]; 2],
|
||||
};
|
||||
apply_variable_depth(&mut m, 0.0, 10.0, 0.2, 1.0);
|
||||
assert!((m.vertices[0][1] - 0.2).abs() < 1e-5, "soffit lift mismatch: {}", m.vertices[0][1]);
|
||||
assert!((m.vertices[1][1] - 1.0).abs() < 1e-5, "top should stay: {}", m.vertices[1][1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_depth_at_support_is_zero() {
|
||||
// 지점(u=0 또는 u=span): lift=0, 변화 없음.
|
||||
let mut m = Mesh {
|
||||
vertices: vec![[0.0, 0.0, 0.0], [0.0, 0.0, 10.0]],
|
||||
indices: vec![],
|
||||
normals: vec![[0.0, 1.0, 0.0]; 2],
|
||||
colors: vec![[0.0; 3]; 2],
|
||||
};
|
||||
apply_variable_depth(&mut m, 0.0, 10.0, 500.0, 1.0);
|
||||
assert_eq!(m.vertices[0][1], 0.0);
|
||||
assert_eq!(m.vertices[1][1], 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ impl<K: GeomKernel + Clone + 'static> IncrementalBridge<K> {
|
||||
|
||||
/// Build the full bridge scene, using IncrementalDb for girder caching.
|
||||
pub fn build_scene(&mut self, params: &SceneParams) -> Result<Mesh, KernelError> {
|
||||
let n = params.girder_count.max(1).min(MAX_GIRDERS);
|
||||
let n = params.girder_count.clamp(1, MAX_GIRDERS);
|
||||
let spacing = params.girder_spacing;
|
||||
let span_m = params.span_m;
|
||||
let h = params.girder_height as f64;
|
||||
|
||||
@@ -24,7 +24,6 @@ use cimery_kernel::OcctKernel;
|
||||
#[cfg(not(feature = "occt"))]
|
||||
use cimery_kernel::PureRustKernel;
|
||||
use camera::{Camera, Projection, StandardView};
|
||||
use glam;
|
||||
use bridge_scene::{
|
||||
GirderSectionType, SceneParams,
|
||||
build_bridge_scene, build_selectable_scene, build_background_scene, scene_extents,
|
||||
@@ -547,7 +546,7 @@ impl RenderState {
|
||||
let mut best: Option<(f32, usize)> = None;
|
||||
for (i, feat) in self.features.iter().enumerate() {
|
||||
if let Some(t) = ray_aabb(near, ray_dir, feat.aabb_min, feat.aabb_max) {
|
||||
if best.map_or(true, |(bt, _)| t < bt) { best = Some((t, i)); }
|
||||
if best.is_none_or(|(bt, _)| t < bt) { best = Some((t, i)); }
|
||||
}
|
||||
}
|
||||
// Update selection + apply highlight
|
||||
@@ -973,9 +972,7 @@ impl ApplicationHandler for CimeryApp {
|
||||
}
|
||||
|
||||
// ── Keyboard shortcuts ────────────────────────────────────────────
|
||||
WindowEvent::KeyboardInput { event: KeyEvent { physical_key, state: key_state, .. }, .. }
|
||||
if key_state == ElementState::Pressed =>
|
||||
{
|
||||
WindowEvent::KeyboardInput { event: KeyEvent { physical_key, state: ElementState::Pressed, .. }, .. } => {
|
||||
match physical_key {
|
||||
PhysicalKey::Code(KeyCode::Escape) => event_loop.exit(),
|
||||
// E → ZoomExtents (fit all)
|
||||
|
||||
@@ -118,14 +118,14 @@ impl ProjectFile {
|
||||
|
||||
pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
|
||||
let json = serde_json::to_string_pretty(self)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
.map_err(std::io::Error::other)?;
|
||||
std::fs::write(path, json)
|
||||
}
|
||||
|
||||
pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
|
||||
let json = std::fs::read_to_string(path)?;
|
||||
serde_json::from_str(&json)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
.map_err(std::io::Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user