Sprint 14: egui TopBottomPanel 리본 + CollapsingHeader SidePanel (상부구조·추가부재·선형·프로젝트) Sprint 15: IncrementalDb 전 Feature 타입 확장 (girder→7종), dirty-tracking 20 unit tests Sprint 16: Gitea + GitHub Actions CI/CD (check/test/clippy/fmt + 멀티플랫폼 릴리스) Sprint 17: AlignmentTransform + AlignmentScene — 선형 국소 프레임 → 세계 좌표 변환 Sprint 18: OcctKernel 교각(16각형 기둥+코핑) + 교대(흉벽+푸팅+날개벽) B-rep Sprint 19: CrossBeamIR + ExpansionJointIR — IR/DSL/kernel/scene 전 계층, sweep_profile_flat_x Sprint 20: 테스트 4층 — Layer1 insta 스냅샷(7종), Layer2 기하 불변량(19), Layer3 두-커널(7), Layer4 proptest(7) — 61 tests pass Sprint 21: cimery-usd PureRustKernel 실제 기하 변환 + BridgeExporter 증분 캐시 Sprint 22: viewer wasm feature + wasm-bindgen/web-sys + GitHub Actions Cloudflare Pages 배포 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
298 lines
10 KiB
Rust
298 lines
10 KiB
Rust
//! Layer 2: Geometric invariants for all kernel feature meshes (Sprint 20).
|
|
//!
|
|
//! Each test verifies physical and topological properties that MUST hold
|
|
//! regardless of which backend produces the mesh:
|
|
//! - vertex_count > 0
|
|
//! - triangle_count > 0
|
|
//! - indices divisible by 3 (well-formed triangle list)
|
|
//! - bounding box spans > 0 on at least one axis
|
|
//! - all normals are (approximately) unit length
|
|
//! - no degenerate triangles (all 3 vertex positions distinct)
|
|
|
|
use cimery_core::{
|
|
AbutmentType, BearingType, ColumnShape, CrossBeamSection, ExpansionJointType,
|
|
MaterialGrade, PierType, SectionType,
|
|
};
|
|
use cimery_ir::{
|
|
AbutmentIR, BearingIR, CapBeamIR, CrossBeamIR, DeckSlabIR, ExpansionJointIR,
|
|
FeatureId, GirderIR, PierIR, PscISectionParams, SectionParams, WingWallIR,
|
|
};
|
|
use cimery_kernel::{GeomKernel, Mesh, PureRustKernel, StubKernel};
|
|
|
|
// ─── Assertion helpers ────────────────────────────────────────────────────────
|
|
|
|
fn assert_valid_mesh(mesh: &Mesh, label: &str) {
|
|
assert!(mesh.vertex_count() > 0,
|
|
"{label}: vertex_count must be > 0");
|
|
assert!(mesh.triangle_count() > 0,
|
|
"{label}: triangle_count must be > 0");
|
|
assert_eq!(mesh.indices.len() % 3, 0,
|
|
"{label}: index count must be divisible by 3");
|
|
assert_eq!(mesh.normals.len(), mesh.vertices.len(),
|
|
"{label}: normals.len() must equal vertices.len()");
|
|
assert_eq!(mesh.colors.len(), mesh.vertices.len(),
|
|
"{label}: colors.len() must equal vertices.len()");
|
|
|
|
// Bounding box must be non-degenerate on at least one axis
|
|
let (mn, mx) = mesh.aabb();
|
|
let extents = [mx[0]-mn[0], mx[1]-mn[1], mx[2]-mn[2]];
|
|
assert!(extents.iter().any(|&e| e > 0.0),
|
|
"{label}: AABB must have positive extent on ≥1 axis, got {:?}", extents);
|
|
|
|
// All normals unit length (tolerance 1e-4 for f32)
|
|
for (i, n) in mesh.normals.iter().enumerate() {
|
|
let len = (n[0]*n[0] + n[1]*n[1] + n[2]*n[2]).sqrt();
|
|
assert!((len - 1.0).abs() < 1e-4,
|
|
"{label}: normal[{i}] length = {len:.6}, expected ~1.0");
|
|
}
|
|
|
|
// All indices in range
|
|
let vcount = mesh.vertex_count() as u32;
|
|
for &idx in &mesh.indices {
|
|
assert!(idx < vcount,
|
|
"{label}: index {idx} out of range (vertex_count = {vcount})");
|
|
}
|
|
}
|
|
|
|
fn assert_span_in_aabb(mesh: &Mesh, expected_span_mm: f32, axis: usize, label: &str) {
|
|
let (mn, mx) = mesh.aabb();
|
|
let actual = mx[axis] - mn[axis];
|
|
assert!((actual - expected_span_mm).abs() < expected_span_mm * 0.01,
|
|
"{label}: span on axis {axis} = {actual:.1} mm, expected {expected_span_mm:.1} mm (±1%)");
|
|
}
|
|
|
|
// ─── IR factories ─────────────────────────────────────────────────────────────
|
|
|
|
fn girder_40m() -> GirderIR {
|
|
GirderIR {
|
|
id: FeatureId::new(),
|
|
station_start: 0.0,
|
|
station_end: 40.0,
|
|
offset_from_alignment: 0.0,
|
|
section_type: SectionType::PscI,
|
|
section: SectionParams::PscI(PscISectionParams::kds_standard()),
|
|
count: 5,
|
|
spacing: 2_500.0,
|
|
material: MaterialGrade::C50,
|
|
}
|
|
}
|
|
|
|
fn deck_slab_40m() -> DeckSlabIR {
|
|
DeckSlabIR {
|
|
id: FeatureId::new(),
|
|
station_start: 0.0,
|
|
station_end: 40.0,
|
|
width_left: 5_500.0,
|
|
width_right: 5_500.0,
|
|
thickness: 220.0,
|
|
haunch_depth: 100.0,
|
|
cross_slope: 2.0,
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
fn bearing_standard() -> BearingIR {
|
|
BearingIR {
|
|
id: FeatureId::new(),
|
|
station: 0.0,
|
|
bearing_type: BearingType::Elastomeric,
|
|
plan_length: 350.0,
|
|
plan_width: 350.0,
|
|
total_height: 90.0,
|
|
capacity_vertical:2_500.0,
|
|
}
|
|
}
|
|
|
|
fn pier_single_column() -> PierIR {
|
|
PierIR {
|
|
id: FeatureId::new(),
|
|
station: 20.0,
|
|
skew_angle: 0.0,
|
|
pier_type: PierType::SingleColumn,
|
|
column_shape: ColumnShape::Circular,
|
|
column_count: 1,
|
|
column_spacing: 0.0,
|
|
column_diameter: 1_500.0,
|
|
column_depth: 0.0,
|
|
column_height: 8_000.0,
|
|
cap_beam: CapBeamIR {
|
|
length: 13_000.0,
|
|
width: 1_200.0,
|
|
depth: 1_400.0,
|
|
cantilever_left: 1_000.0,
|
|
cantilever_right:1_000.0,
|
|
},
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
fn abutment_standard() -> AbutmentIR {
|
|
AbutmentIR {
|
|
id: FeatureId::new(),
|
|
station: 0.0,
|
|
skew_angle: 0.0,
|
|
abutment_type: AbutmentType::ReverseT,
|
|
breast_wall_height: 5_000.0,
|
|
breast_wall_thickness: 800.0,
|
|
breast_wall_width: 12_000.0,
|
|
footing_length: 4_000.0,
|
|
footing_width: 13_000.0,
|
|
footing_thickness: 1_000.0,
|
|
wing_wall_left: WingWallIR { length: 4_000.0, height: 4_000.0, thickness: 500.0 },
|
|
wing_wall_right: WingWallIR { length: 4_000.0, height: 4_000.0, thickness: 500.0 },
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
fn cross_beam_standard() -> CrossBeamIR {
|
|
CrossBeamIR {
|
|
id: FeatureId::new(),
|
|
station: 10.0,
|
|
section: CrossBeamSection::HSection,
|
|
web_height: 1_260.0,
|
|
web_thickness: 12.0,
|
|
flange_width: 300.0,
|
|
flange_thickness: 16.0,
|
|
bay_count: 4,
|
|
girder_spacing: 2_500.0,
|
|
material: MaterialGrade::Ss400,
|
|
}
|
|
}
|
|
|
|
fn expansion_joint_standard() -> ExpansionJointIR {
|
|
ExpansionJointIR {
|
|
id: FeatureId::new(),
|
|
station: 0.0,
|
|
joint_type: ExpansionJointType::RubberType,
|
|
gap_width: 50.0,
|
|
total_width: 11_000.0,
|
|
depth: 100.0,
|
|
movement_range: 30.0,
|
|
}
|
|
}
|
|
|
|
// ─── PureRustKernel invariants ────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn prk_girder_valid_mesh() {
|
|
let mesh = PureRustKernel.girder_mesh(&girder_40m()).unwrap();
|
|
assert_valid_mesh(&mesh, "PureRustKernel::girder");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_girder_span_correct() {
|
|
let mesh = PureRustKernel.girder_mesh(&girder_40m()).unwrap();
|
|
assert_span_in_aabb(&mesh, 40_000.0, 2, "PureRustKernel::girder span (Z)");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_deck_slab_valid_mesh() {
|
|
let mesh = PureRustKernel.deck_slab_mesh(&deck_slab_40m()).unwrap();
|
|
assert_valid_mesh(&mesh, "PureRustKernel::deck_slab");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_deck_slab_span_correct() {
|
|
let mesh = PureRustKernel.deck_slab_mesh(&deck_slab_40m()).unwrap();
|
|
assert_span_in_aabb(&mesh, 40_000.0, 2, "PureRustKernel::deck_slab span (Z)");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_bearing_valid_mesh() {
|
|
let mesh = PureRustKernel.bearing_mesh(&bearing_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "PureRustKernel::bearing");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_pier_valid_mesh() {
|
|
let mesh = PureRustKernel.pier_mesh(&pier_single_column()).unwrap();
|
|
assert_valid_mesh(&mesh, "PureRustKernel::pier");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_abutment_valid_mesh() {
|
|
let mesh = PureRustKernel.abutment_mesh(&abutment_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "PureRustKernel::abutment");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_cross_beam_valid_mesh() {
|
|
let mesh = PureRustKernel.cross_beam_mesh(&cross_beam_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "PureRustKernel::cross_beam");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_cross_beam_length_correct() {
|
|
let ir = cross_beam_standard();
|
|
let mesh = PureRustKernel.cross_beam_mesh(&ir).unwrap();
|
|
let expected = ir.total_length_mm() as f32;
|
|
// Cross beams sweep along X axis
|
|
assert_span_in_aabb(&mesh, expected, 0, "PureRustKernel::cross_beam length (X)");
|
|
}
|
|
|
|
#[test]
|
|
fn prk_expansion_joint_valid_mesh() {
|
|
let mesh = PureRustKernel.expansion_joint_mesh(&expansion_joint_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "PureRustKernel::expansion_joint");
|
|
}
|
|
|
|
// ─── StubKernel invariants ────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn stub_girder_valid_mesh() {
|
|
let mesh = StubKernel.girder_mesh(&girder_40m()).unwrap();
|
|
assert_valid_mesh(&mesh, "StubKernel::girder");
|
|
}
|
|
|
|
#[test]
|
|
fn stub_deck_slab_valid_mesh() {
|
|
let mesh = StubKernel.deck_slab_mesh(&deck_slab_40m()).unwrap();
|
|
assert_valid_mesh(&mesh, "StubKernel::deck_slab");
|
|
}
|
|
|
|
#[test]
|
|
fn stub_bearing_valid_mesh() {
|
|
let mesh = StubKernel.bearing_mesh(&bearing_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "StubKernel::bearing");
|
|
}
|
|
|
|
#[test]
|
|
fn stub_pier_valid_mesh() {
|
|
let mesh = StubKernel.pier_mesh(&pier_single_column()).unwrap();
|
|
assert_valid_mesh(&mesh, "StubKernel::pier");
|
|
}
|
|
|
|
#[test]
|
|
fn stub_abutment_valid_mesh() {
|
|
let mesh = StubKernel.abutment_mesh(&abutment_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "StubKernel::abutment");
|
|
}
|
|
|
|
#[test]
|
|
fn stub_cross_beam_valid_mesh() {
|
|
let mesh = StubKernel.cross_beam_mesh(&cross_beam_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "StubKernel::cross_beam");
|
|
}
|
|
|
|
#[test]
|
|
fn stub_expansion_joint_valid_mesh() {
|
|
let mesh = StubKernel.expansion_joint_mesh(&expansion_joint_standard()).unwrap();
|
|
assert_valid_mesh(&mesh, "StubKernel::expansion_joint");
|
|
}
|
|
|
|
// ─── Error cases ──────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn stub_zero_span_returns_error() {
|
|
let mut g = girder_40m();
|
|
g.station_end = g.station_start;
|
|
assert!(StubKernel.girder_mesh(&g).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn prk_zero_span_returns_error() {
|
|
let mut g = girder_40m();
|
|
g.station_end = g.station_start;
|
|
assert!(PureRustKernel.girder_mesh(&g).is_err());
|
|
}
|