use super::GeometryRouter;
use ifc_lite_core::EntityDecoder;
fn fixture(kind: &str, rep: &str) -> String {
format!("#1=IFCCARTESIANPOINT((2.,3.));#2=IFCCARTESIANPOINT((6.,3.));#3=IFCCARTESIANPOINT((6.,7.));#4=IFCCARTESIANPOINT((2.,7.));
#5=IFCCARTESIANPOINT((3.,4.));#6=IFCCARTESIANPOINT((4.,4.));#7=IFCCARTESIANPOINT((4.,5.));#8=IFCCARTESIANPOINT((3.,5.));
#10=IFCPOLYLINE((#1,#2,#3,#4,#1));#11=IFCPOLYLINE((#5,#6,#7,#8,#5));#12=IFCANNOTATIONFILLAREA(#10,(#11));
#20=IFCSHAPEREPRESENTATION($,'Annotation','{rep}',(#12));#21=IFCPRODUCTDEFINITIONSHAPE($,$,(#20));
#30=IFCCARTESIANPOINT((10.,20.,30.));#31=IFCDIRECTION((0.,1.,0.));#32=IFCDIRECTION((1.,0.,0.));#33=IFCAXIS2PLACEMENT3D(#30,#31,#32);#34=IFCLOCALPLACEMENT($,#33);
#40={kind}('0000000000000000000000',$,'fill',$,$,#34,#21);")
}
#[test]
fn annotation_fill_4406_hole_vertical_placement_and_units() {
let source = fixture("IFCANNOTATION", "Annotation2D");
let mut decoder = EntityDecoder::new(&source);
let router = GeometryRouter::with_scale(0.001);
let entity = decoder.decode_by_id(40).unwrap();
let parts = router
.process_element_with_submeshes(&entity, &mut decoder)
.unwrap();
assert_eq!(parts.sub_meshes.len(), 1);
let mesh = &parts.sub_meshes[0].mesh;
assert_eq!(mesh.triangle_count(), 8);
let mut area = 0.;
for tri in mesh.indices.chunks_exact(3) {
let pts: Vec<_> = tri
.iter()
.map(|i| {
nalgebra::Vector3::new(
mesh.positions[*i as usize * 3] as f64,
mesh.positions[*i as usize * 3 + 1] as f64,
mesh.positions[*i as usize * 3 + 2] as f64,
)
})
.collect();
area += (pts[1] - pts[0]).cross(&(pts[2] - pts[0])).norm() * 0.5;
let center = (pts[0] + pts[1] + pts[2]) / 3.;
assert!(!(center.x > 0.013 && center.x < 0.014 && center.z > 0.025 && center.z < 0.026));
}
assert!((area - 15e-6).abs() < 1e-10, "{area}");
assert!(mesh
.positions
.chunks_exact(3)
.all(|p| (p[1] as f64 + mesh.origin[1] - 0.020).abs() < 1e-8));
}
#[test]
fn annotation_fill_4406_non_annotation_auxiliary_is_absent() {
for rep in ["FootPrint", "Annotation2D", "Surface2D"] {
let source = fixture("IFCWALL", rep);
let mut decoder = EntityDecoder::new(&source);
let entity = decoder.decode_by_id(40).unwrap();
assert!(GeometryRouter::new()
.process_element_with_submeshes(&entity, &mut decoder)
.unwrap()
.is_empty());
}
}
#[test]
fn annotation_fill_4406_malformed_ring_refuses_and_reports() {
let original = fixture("IFCANNOTATION", "Annotation2D");
for source in [
original.replace("#1,#2,#3,#4,#1", "#1,#3,#2,#4,#1"),
original.replace("#1,#2,#3,#4,#1", "#1,#2,#3,#4"),
original.replace(
"IFCPOLYLINE((#1,#2,#3,#4,#1))",
"IFCPOLYLINE((#10,#2,#3,#4,#10))",
),
] {
let mut decoder = EntityDecoder::new(&source);
let entity = decoder.decode_by_id(40).unwrap();
let router = GeometryRouter::new();
assert!(router
.process_element_with_submeshes(&entity, &mut decoder)
.is_err());
assert_eq!(
router.take_unsupported_items().get("IfcAnnotationFillArea"),
Some(&1)
);
}
}
#[test]
fn annotation_fill_4406_outside_nested_nonplanar_and_budget_refusals() {
let original = fixture("IFCANNOTATION", "Annotation2D");
let cases = [
original.replace("((3.,4.))", "((9.,4.))"),
original.replace("((3.,4.))", "((3.,4.,1.))"),
original.replace("(#11));", "(#11,#11));"),
original.replace("#1,#2,#3,#4,#1", &vec!["#1"; 2049].join(",")),
];
for source in cases {
let mut decoder = EntityDecoder::new(&source);
let entity = decoder.decode_by_id(40).unwrap();
let router = GeometryRouter::new();
assert!(router
.process_element_with_submeshes(&entity, &mut decoder)
.is_err());
assert_eq!(
router.take_unsupported_items().get("IfcAnnotationFillArea"),
Some(&1)
);
}
}
#[test]
fn annotation_fill_4406_unsupported_paint_refuses_with_diagnostic() {
let base = fixture("IFCANNOTATION", "Annotation2D");
let style = "#60=IFCCOLOURRGB($,0.2,0.6,0.8);#61=IFCFILLAREASTYLE('paint',(#60),.F.);#62=IFCSTYLEDITEM(#12,(#61),$);";
for suffix in [
style.replace("0.2,0.6", "1.2,0.6"),
style.replace("0.2,0.6", "1.E300,0.6"),
style.replace("0.2,0.6", "1.E999,0.6"),
style.replace("(#60),.F.", "(#60,#63),.F.") + "#63=IFCFILLAREASTYLEHATCHING($,$,$,$,$);",
style.replace("(#60),.F.", "(#60,#60),.F."),
style.replace(
"#60=IFCCOLOURRGB($,0.2,0.6,0.8)",
"#60=IFCFILLAREASTYLEHATCHING($,$,$,$,$)",
),
style.replace("(#61),$", &format!("({}),$", vec!["#61"; 65].join(","))),
style.replace("(#61),$", "(#63),$") + "#63=IFCPRESENTATIONSTYLEASSIGNMENT((#63));",
] {
let source = base.clone() + &suffix;
let mut decoder = EntityDecoder::new(&source);
let entity = decoder.decode_by_id(40).unwrap();
let router = GeometryRouter::new();
assert!(router
.process_element_with_submeshes(&entity, &mut decoder)
.is_err());
assert_eq!(
router.take_unsupported_items().get("IfcAnnotationFillArea"),
Some(&1)
);
}
}
#[test]
fn annotation_fill_4406_reversed_rings_keep_area_and_normal_winding() {
let original = fixture("IFCANNOTATION", "Annotation2D");
for source in [
original.clone(),
original.replace("#1,#2,#3,#4,#1", "#1,#4,#3,#2,#1"),
original.replace("#5,#6,#7,#8,#5", "#5,#8,#7,#6,#5"),
] {
let mut decoder = EntityDecoder::new(&source);
let entity = decoder.decode_by_id(40).unwrap();
let parts = GeometryRouter::new()
.process_element_with_submeshes(&entity, &mut decoder)
.unwrap();
let mesh = &parts.sub_meshes[0].mesh;
let mut area = 0.;
for tri in mesh.indices.chunks_exact(3) {
let p: Vec<_> = tri
.iter()
.map(|i| {
nalgebra::Vector3::from_column_slice(
&mesh.positions[*i as usize * 3..*i as usize * 3 + 3],
)
})
.collect();
let cross = (p[1] - p[0]).cross(&(p[2] - p[0]));
area += cross.norm() * 0.5;
let n = nalgebra::Vector3::from_column_slice(
&mesh.normals[tri[0] as usize * 3..tri[0] as usize * 3 + 3],
);
assert!(cross.dot(&n) > 0., "triangle and normal must agree");
}
assert!((area - 15.).abs() < 1e-5);
}
}