use super::GeometryRouter;
use ifc_lite_core::EntityDecoder;
#[test]
fn test_router_creation() {
let router = GeometryRouter::new();
assert!(!router.processors.is_empty());
}
#[test]
fn test_parse_cartesian_point() {
let content = r#"
#1=IFCCARTESIANPOINT((100.0,200.0,300.0));
#2=IFCWALL('guid',$,$,$,$,$,#1,$);
"#;
let mut decoder = EntityDecoder::new(content);
let router = GeometryRouter::new();
let wall = decoder.decode_by_id(2).unwrap();
let point = router
.parse_cartesian_point(&wall, &mut decoder, 6)
.unwrap();
assert_eq!(point.x, 100.0);
assert_eq!(point.y, 200.0);
assert_eq!(point.z, 300.0);
}
#[test]
fn test_parse_direction() {
let content = r#"
#1=IFCDIRECTION((1.0,0.0,0.0));
"#;
let mut decoder = EntityDecoder::new(content);
let router = GeometryRouter::new();
let direction = decoder.decode_by_id(1).unwrap();
let vec = router.parse_direction(&direction).unwrap();
assert_eq!(vec.x, 1.0);
assert_eq!(vec.y, 0.0);
assert_eq!(vec.z, 0.0);
}
mod wall_profile_research {
use crate::bool2d::subtract_2d;
use crate::extrusion::extrude_profile;
use crate::profile::Profile2D;
use crate::router::GeometryRouter;
use crate::Point3;
use nalgebra::Point2;
#[test]
fn test_chamfered_footprint_extrusion() {
let footprint = Profile2D::new(vec![
Point2::new(0.300, -0.300), Point2::new(9.700, -0.300), Point2::new(10.000, 0.000), Point2::new(0.000, 0.000), Point2::new(0.300, -0.300), ]);
let mesh = extrude_profile(&footprint, 2.7, None).unwrap();
assert!(mesh.vertex_count() > 0);
assert!(mesh.triangle_count() > 0);
let (min, max) = mesh.bounds();
assert!((min.x - 0.0).abs() < 0.01);
assert!((max.x - 10.0).abs() < 0.01);
assert!((min.y - (-0.3)).abs() < 0.01);
assert!((max.y - 0.0).abs() < 0.01);
assert!((min.z - 0.0).abs() < 0.01);
assert!((max.z - 2.7).abs() < 0.01);
assert!(mesh.vertex_count() >= 20);
}
#[test]
fn test_coordinate_system_analysis() {
let footprint_profile = Profile2D::new(vec![
Point2::new(0.3, -0.3), Point2::new(9.7, -0.3), Point2::new(10.0, 0.0), Point2::new(0.0, 0.0), ]);
let wall_face_profile = Profile2D::new(vec![
Point2::new(0.0, 0.0), Point2::new(10.0, 0.0), Point2::new(10.0, 2.7), Point2::new(0.0, 2.7), ]);
let footprint_bounds = footprint_profile.outer.iter().fold(
(f64::MAX, f64::MAX, f64::MIN, f64::MIN),
|(min_x, min_y, max_x, max_y), p| {
(
min_x.min(p.x),
min_y.min(p.y),
max_x.max(p.x),
max_y.max(p.y),
)
},
);
let face_bounds = wall_face_profile.outer.iter().fold(
(f64::MAX, f64::MAX, f64::MIN, f64::MIN),
|(min_x, min_y, max_x, max_y), p| {
(
min_x.min(p.x),
min_y.min(p.y),
max_x.max(p.x),
max_y.max(p.y),
)
},
);
let _footprint_bounds = footprint_bounds; let _face_bounds = face_bounds;
assert!((footprint_bounds.2 - footprint_bounds.0 - 10.0).abs() < 0.01);
assert!((face_bounds.2 - face_bounds.0 - 10.0).abs() < 0.01);
}
#[test]
fn test_opening_projection_strategy() {
let opening_face_min_u = 6.495; let opening_face_min_v = 0.8; let opening_face_max_u = 8.495; let opening_face_max_v = 2.0;
let mut wall_face = Profile2D::new(vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 2.7),
Point2::new(0.0, 2.7),
]);
wall_face.add_hole(vec![
Point2::new(opening_face_min_u, opening_face_min_v),
Point2::new(opening_face_max_u, opening_face_min_v),
Point2::new(opening_face_max_u, opening_face_max_v),
Point2::new(opening_face_min_u, opening_face_max_v),
]);
let mesh_with_opening = extrude_profile(&wall_face, 0.3, None).unwrap();
assert!(mesh_with_opening.vertex_count() > 0);
}
#[test]
fn test_efficient_2d_boolean_approach() {
let wall_face = Profile2D::new(vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 2.7),
Point2::new(0.0, 2.7),
]);
let opening_contour = vec![
Point2::new(6.495, 0.8),
Point2::new(8.495, 0.8),
Point2::new(8.495, 2.0),
Point2::new(6.495, 2.0),
];
let wall_with_opening = subtract_2d(&wall_face, &opening_contour).unwrap();
assert_eq!(wall_with_opening.holes.len(), 1);
assert_eq!(wall_with_opening.holes[0].len(), 4);
let mesh = extrude_profile(&wall_with_opening, 0.3, None).unwrap();
assert!(mesh.vertex_count() < 200);
}
#[test]
fn test_hybrid_plane_clipping_approach() {
use crate::csg::ClippingProcessor;
let chamfered_footprint = Profile2D::new(vec![
Point2::new(0.3, -0.3),
Point2::new(9.7, -0.3),
Point2::new(10.0, 0.0),
Point2::new(0.0, 0.0),
]);
let chamfered_wall = extrude_profile(&chamfered_footprint, 2.7, None).unwrap();
let initial_vertex_count = chamfered_wall.vertex_count();
let initial_triangle_count = chamfered_wall.triangle_count();
let opening_min_u = 6.495;
let opening_max_u = 8.495;
let opening_min_v = 0.8;
let opening_max_v = 2.0;
let clipper = ClippingProcessor::new();
let opening_min = Point3::new(opening_min_u, -0.3, opening_min_v);
let opening_max = Point3::new(opening_max_u, 0.0, opening_max_v);
let result = clipper
.subtract_box(&chamfered_wall, opening_min, opening_max)
.unwrap();
let final_vertex_count = result.vertex_count();
let final_triangle_count = result.triangle_count();
assert!(final_vertex_count > initial_vertex_count);
let (_min, max) = result.bounds();
assert!((max.x - 10.0).abs() < 0.1);
println!(
"Hybrid approach: {} verts, {} tris (was {} verts, {} tris)",
final_vertex_count, final_triangle_count, initial_vertex_count, initial_triangle_count
);
}
#[test]
fn test_benchmark_comparison() {
use crate::csg::ClippingProcessor;
let chamfered_footprint = Profile2D::new(vec![
Point2::new(0.3, -0.3),
Point2::new(9.7, -0.3),
Point2::new(10.0, 0.0),
Point2::new(0.0, 0.0),
]);
let mesh_a = extrude_profile(&chamfered_footprint, 2.7, None).unwrap();
let verts_a = mesh_a.vertex_count();
let tris_a = mesh_a.triangle_count();
let mut wall_face = Profile2D::new(vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 2.7),
Point2::new(0.0, 2.7),
]);
wall_face.add_hole(vec![
Point2::new(1.2, 0.8),
Point2::new(2.2, 0.8),
Point2::new(2.2, 2.0),
Point2::new(1.2, 2.0),
]);
wall_face.add_hole(vec![
Point2::new(4.5, 0.8),
Point2::new(5.5, 0.8),
Point2::new(5.5, 2.0),
Point2::new(4.5, 2.0),
]);
wall_face.add_hole(vec![
Point2::new(7.8, 0.8),
Point2::new(8.8, 0.8),
Point2::new(8.8, 2.0),
Point2::new(7.8, 2.0),
]);
let mesh_b = extrude_profile(&wall_face, 0.3, None).unwrap();
let verts_b = mesh_b.vertex_count();
let tris_b = mesh_b.triangle_count();
let clipper = ClippingProcessor::new();
let mut mesh_c = mesh_a.clone();
let openings = vec![
(1.2, 0.8, 2.2, 2.0),
(4.5, 0.8, 5.5, 2.0),
(7.8, 0.8, 8.8, 2.0),
];
for (min_u, min_v, max_u, max_v) in openings {
let opening_min = Point3::new(min_u, -0.3, min_v);
let opening_max = Point3::new(max_u, 0.0, max_v);
mesh_c = clipper
.subtract_box(&mesh_c, opening_min, opening_max)
.unwrap();
}
let verts_c = mesh_c.vertex_count();
let tris_c = mesh_c.triangle_count();
println!("\n=== Benchmark Comparison ===");
println!(
"Approach A (chamfered, no openings): {} verts, {} tris",
verts_a, tris_a
);
println!(
"Approach B (rectangular, with openings): {} verts, {} tris",
verts_b, tris_b
);
println!(
"Approach C (hybrid, chamfered + openings): {} verts, {} tris",
verts_c, tris_c
);
println!("\nKey Insights:");
println!("- Approach B loses chamfers (not acceptable)");
println!("- Approach C preserves chamfers AND adds openings");
println!(
"- Approach C vertex count: {} (target: <200 for efficiency)",
verts_c
);
assert!(verts_b > verts_a);
let (_min_c, max_c) = mesh_c.bounds();
assert!((max_c.x - 10.0).abs() < 0.1);
assert!(
verts_c < 700,
"Hybrid approach should be more efficient than full CSG"
);
}
#[test]
fn test_optimized_implementation_benchmark() {
use crate::csg::ClippingProcessor;
let chamfered_footprint = Profile2D::new(vec![
Point2::new(0.3, -0.3),
Point2::new(9.7, -0.3),
Point2::new(10.0, 0.0),
Point2::new(0.0, 0.0),
]);
let wall_mesh = extrude_profile(&chamfered_footprint, 2.7, None).unwrap();
let initial_verts = wall_mesh.vertex_count();
let initial_tris = wall_mesh.triangle_count();
let open_min = Point3::new(6.495, -0.3, 0.8);
let open_max = Point3::new(8.495, 0.0, 2.0);
let (wall_min_f32, wall_max_f32) = wall_mesh.bounds();
let wall_min = Point3::new(
wall_min_f32.x as f64,
wall_min_f32.y as f64,
wall_min_f32.z as f64,
);
let wall_max = Point3::new(
wall_max_f32.x as f64,
wall_max_f32.y as f64,
wall_max_f32.z as f64,
);
let clipper = ClippingProcessor::new();
let csg_result = clipper
.subtract_box(&wall_mesh, open_min, open_max)
.unwrap();
let csg_verts = csg_result.vertex_count();
let csg_tris = csg_result.triangle_count();
let router = GeometryRouter::new();
let opt_result = router.cut_rectangular_opening(&wall_mesh, open_min, open_max);
let opt_verts = opt_result.vertex_count();
let opt_tris = opt_result.triangle_count();
println!("\n=== Optimized vs CSG Comparison ===");
println!(
"Initial wall: {} verts, {} tris",
initial_verts, initial_tris
);
println!("CSG approach: {} verts, {} tris", csg_verts, csg_tris);
println!("Optimized approach: {} verts, {} tris", opt_verts, opt_tris);
assert!(csg_result.vertex_count() > 0);
assert!(opt_result.vertex_count() > 0);
let (_csg_min, csg_max) = csg_result.bounds();
let (_opt_min, opt_max) = opt_result.bounds();
assert!((csg_max.x - 10.0).abs() < 0.1);
assert!((opt_max.x - 10.0).abs() < 0.1);
}
#[test]
fn test_chamfer_preservation_analysis() {
let chamfered = Profile2D::new(vec![
Point2::new(0.3, -0.3), Point2::new(9.7, -0.3), Point2::new(10.0, 0.0), Point2::new(0.0, 0.0), ]);
let rectangular = Profile2D::new(vec![
Point2::new(0.0, -0.3),
Point2::new(10.0, -0.3),
Point2::new(10.0, 0.0),
Point2::new(0.0, 0.0),
]);
let mesh_chamfered = extrude_profile(&chamfered, 2.7, None).unwrap();
let mesh_rectangular = extrude_profile(&rectangular, 2.7, None).unwrap();
assert!(mesh_chamfered.vertex_count() >= mesh_rectangular.vertex_count());
let (_, max_chamfered) = mesh_chamfered.bounds();
let (_, max_rectangular) = mesh_rectangular.bounds();
assert!((max_chamfered.z - max_rectangular.z).abs() < 0.01);
}
}
mod infra_rtc_detection {
use super::*;
fn infra_model_ifc() -> String {
r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[Ifc4x3NotAssigned]'),'2;1');
FILE_NAME('test.ifc','2025-04-03T20:15:31',(''),(''),'','12d Model','');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPROJECT('3A_FOM1U13fh337NmQeVRd',$,'TestProject','',$,$,$,(#12),#7);
#7=IFCUNITASSIGNMENT((#8));
#8=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#12=IFCGEOMETRICREPRESENTATIONCONTEXT('3D','Model',3,1.E-6,#14,$);
#13=IFCLOCALPLACEMENT($,#14);
#14=IFCAXIS2PLACEMENT3D(#15,#16,#17);
#15=IFCCARTESIANPOINT((0.,0.,0.));
#16=IFCDIRECTION((0.,0.,1.));
#17=IFCDIRECTION((1.,0.,0.));
#37=IFCSITE('1hW4TzF_DDAfTPaQBppMz3',$,'Site','',$,#13,$,$,.ELEMENT.,$,$,$,$,$);
#38=IFCRELAGGREGATES('1QP4NryH5APR64IuPmfbrw',$,'','',#1,(#37));
#39=IFCFACILITY('3fh5t6Rfv4KgZVJyIsS3vL',$,'TestFacility','',$,#13,$,$,.ELEMENT.);
#40=IFCRELAGGREGATES('0JznlPoAL2t9gXdhqZciud',$,'','',#37,(#39));
#41=IFCRELCONTAINEDINSPATIALSTRUCTURE('2nyGDMmiP47BqaRKBUVTUc',$,'','FacilityContainer',(#42),#39);
#42=IFCBUILDINGELEMENTPROXY('2JJeX0xY93XxwyMxv0upiL',$,'Trimesh','12d Trimesh','Trimesh',#13,#43,$,.USERDEFINED.);
#43=IFCPRODUCTDEFINITIONSHAPE($,$,(#44));
#44=IFCSHAPEREPRESENTATION(#12,'Body','Brep',(#100));
#100=IFCFACETEDBREP(#101);
#101=IFCCLOSEDSHELL((#102));
#102=IFCFACE((#103));
#103=IFCFACEOUTERBOUND(#104,.T.);
#104=IFCPOLYLOOP((#110,#111,#112));
#110=IFCCARTESIANPOINT((280964.209858276,6214442.15622959,145.312878290516));
#111=IFCCARTESIANPOINT((280966.589503645,6214441.40182406,145.321540679517));
#112=IFCCARTESIANPOINT((280968.964944952,6214440.62254459,145.330215679517));
ENDSEC;
END-ISO-10303-21;
"#
.to_string()
}
fn infra_model_ifc_b() -> String {
r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition[Ifc4x3NotAssigned]'),'2;1');
FILE_NAME('test_b.ifc','2025-04-03T20:15:31',(''),(''),'','12d Model','');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPROJECT('3A_FOM1U13fh337NmQeVRd',$,'TestProject','',$,$,$,(#12),#7);
#7=IFCUNITASSIGNMENT((#8));
#8=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#12=IFCGEOMETRICREPRESENTATIONCONTEXT('3D','Model',3,1.E-6,#14,$);
#13=IFCLOCALPLACEMENT($,#14);
#14=IFCAXIS2PLACEMENT3D(#15,#16,#17);
#15=IFCCARTESIANPOINT((0.,0.,0.));
#16=IFCDIRECTION((0.,0.,1.));
#17=IFCDIRECTION((1.,0.,0.));
#37=IFCSITE('0AvQ9WiKj9QhhBF8HoQbpT',$,'Site','',$,#13,$,$,.ELEMENT.,$,$,$,$,$);
#38=IFCRELAGGREGATES('0cPQjCyWf38RWxUzqd9LMm',$,'','',#1,(#37));
#39=IFCFACILITY('0kH5sw_GL2axycWUi$aMhv',$,'TestFacility','',$,#13,$,$,.ELEMENT.);
#40=IFCRELAGGREGATES('2ZShpA4fL9QObco6Upayde',$,'','',#37,(#39));
#41=IFCRELCONTAINEDINSPATIALSTRUCTURE('17fDKZ7VHE590ShtaZSobA',$,'','FacilityContainer',(#42),#39);
#42=IFCBUILDINGELEMENTPROXY('348HbFCG9ESeA2m3bPTUIP',$,'Trimesh','12d Trimesh','Trimesh',#13,#43,$,.USERDEFINED.);
#43=IFCPRODUCTDEFINITIONSHAPE($,$,(#44));
#44=IFCSHAPEREPRESENTATION(#12,'Body','Brep',(#100));
#100=IFCFACETEDBREP(#101);
#101=IFCCLOSEDSHELL((#102));
#102=IFCFACE((#103));
#103=IFCFACEOUTERBOUND(#104,.T.);
#104=IFCPOLYLOOP((#110,#111,#112));
#110=IFCCARTESIANPOINT((279616.962383915,6213394.41079812,222.904072802032));
#111=IFCCARTESIANPOINT((279617.172274625,6213389.48119807,222.626516208578));
#112=IFCCARTESIANPOINT((279617.409779591,6213384.48685233,222.345251208578));
ENDSEC;
END-ISO-10303-21;
"#
.to_string()
}
#[test]
fn rtc_detected_from_geometry_vertices_not_just_placement() {
let content = infra_model_ifc();
let entity_index = ifc_lite_core::build_entity_index(&content);
let mut decoder = EntityDecoder::with_index(&content, entity_index);
let router = GeometryRouter::with_units(&content, &mut decoder);
let offset = router.detect_rtc_offset_from_first_element(&content, &mut decoder);
assert!(
offset.0.abs() > 10000.0 || offset.1.abs() > 10000.0,
"RTC offset should be large for infrastructure model, got ({:.1}, {:.1}, {:.1})",
offset.0,
offset.1,
offset.2
);
assert!(
(offset.0 - 280966.0).abs() < 100.0,
"X offset should be near 280966, got {:.1}",
offset.0
);
assert!(
(offset.1 - 6214441.0).abs() < 100.0,
"Y offset should be near 6214441, got {:.1}",
offset.1
);
}
#[test]
fn rtc_produces_small_vertex_coordinates() {
let content = infra_model_ifc();
let entity_index = ifc_lite_core::build_entity_index(&content);
let mut decoder = EntityDecoder::with_index(&content, entity_index);
let mut router = GeometryRouter::with_units(&content, &mut decoder);
let offset = router.detect_rtc_offset_from_first_element(&content, &mut decoder);
router.set_rtc_offset(offset);
let entity = decoder.decode_by_id(42).unwrap();
let mesh = router.process_element(&entity, &mut decoder).unwrap();
for chunk in mesh.positions.chunks_exact(3) {
assert!(
chunk[0].abs() < 10000.0 && chunk[1].abs() < 10000.0 && chunk[2].abs() < 10000.0,
"Vertex ({}, {}, {}) still has large coordinates after RTC",
chunk[0],
chunk[1],
chunk[2]
);
}
}
#[test]
fn federated_models_produce_usable_rtc_offsets() {
let content_a = infra_model_ifc();
let content_b = infra_model_ifc_b();
let entity_index_a = ifc_lite_core::build_entity_index(&content_a);
let mut decoder_a = EntityDecoder::with_index(&content_a, entity_index_a);
let router_a = GeometryRouter::with_units(&content_a, &mut decoder_a);
let offset_a = router_a.detect_rtc_offset_from_first_element(&content_a, &mut decoder_a);
let entity_index_b = ifc_lite_core::build_entity_index(&content_b);
let mut decoder_b = EntityDecoder::with_index(&content_b, entity_index_b);
let router_b = GeometryRouter::with_units(&content_b, &mut decoder_b);
let offset_b = router_b.detect_rtc_offset_from_first_element(&content_b, &mut decoder_b);
assert!(
offset_a.0.abs() > 10000.0,
"Model A should have large X offset"
);
assert!(
offset_b.0.abs() > 10000.0,
"Model B should have large X offset"
);
let delta_x = offset_a.0 - offset_b.0;
let delta_y = offset_a.1 - offset_b.1;
let delta_z = offset_a.2 - offset_b.2;
assert!(
delta_x.abs() < 5000.0,
"X delta between models should be reasonable, got {:.1}",
delta_x
);
assert!(
delta_y.abs() < 5000.0,
"Y delta between models should be reasonable, got {:.1}",
delta_y
);
let delta_x_f32 = delta_x as f32;
let delta_y_f32 = delta_y as f32;
assert!(
(delta_x_f32 as f64 - delta_x).abs() < 1.0,
"RTC delta X should survive f32 round-trip"
);
assert!(
(delta_y_f32 as f64 - delta_y).abs() < 1.0,
"RTC delta Y should survive f32 round-trip"
);
}
}