//! 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()); }