use ifc_lite_processing::extract_symbolic_data;
fn fixture(extra_entities: &str, axis2_ref: &str) -> String {
format!(
r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('issue-1994 fixture'),'2;1');
FILE_NAME('test.ifc','2026-08-03T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('0$ScRe4drECQ4DMSqUjd6d',$,'P',$,$,$,$,(#2),#3);
#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,#5,$);
#3=IFCUNITASSIGNMENT((#6));
#4=IFCCARTESIANPOINT((0.,0.,0.));
#5=IFCAXIS2PLACEMENT3D(#4,$,$);
#6=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#10=IFCCARTESIANPOINT((0.,0.));
#11=IFCCARTESIANPOINT((1.,2.));
#17=IFCCARTESIANPOINT((3.,1.));
#12=IFCPOLYLINE((#10,#11,#17));
#13=IFCSHAPEREPRESENTATION(#2,'Body','GeometricCurveSet',(#12));
#14=IFCCARTESIANPOINT((0.,0.));
#15=IFCAXIS2PLACEMENT3D(#14,$,$);
#16=IFCREPRESENTATIONMAP(#15,#13);
#20=IFCDIRECTION((1.,0.));
#21=IFCCARTESIANPOINT((0.,0.));
{extra_entities}
#22=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#20,{axis2_ref},#21,$);
#30=IFCMAPPEDITEM(#16,#22);
#40=IFCLOCALPLACEMENT($,#5);
#50=IFCSHAPEREPRESENTATION(#2,'Annotation','MappedRepresentation',(#30));
#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
#52=IFCANNOTATION('1xScRe4drECQ4DMSqUjd6d',$,'Note',$,$,#40,#51);
ENDSEC;
END-ISO-10303-21;
"#
)
}
#[test]
fn non_mirroring_axis2_null_is_unchanged() {
let data = extract_symbolic_data(&fixture("", "$"));
let pl = data
.polylines
.iter()
.find(|p| p.representation == "Annotation")
.expect("mapped annotation polyline");
assert_eq!(pl.points.len(), 6, "3-point polyline -> 6 floats");
assert_eq!((pl.points[0], pl.points[1]), (0.0, 0.0), "start point");
let (ex, ey) = (pl.points[2], pl.points[3]);
assert!((ex - 1.0).abs() < 1e-5, "non-mirrored endpoint x: {ex}");
assert!((ey - -2.0).abs() < 1e-5, "non-mirrored endpoint y: {ey}");
}
#[test]
fn mirroring_axis2_flips_the_mapped_geometry() {
let content = fixture("#23=IFCDIRECTION((0.,-1.));", "#23");
let data = extract_symbolic_data(&content);
let pl = data
.polylines
.iter()
.find(|p| p.representation == "Annotation")
.expect("mapped annotation polyline");
assert_eq!(pl.points.len(), 6, "3-point polyline -> 6 floats");
assert_eq!((pl.points[0], pl.points[1]), (0.0, 0.0), "start point");
let (ex, ey) = (pl.points[2], pl.points[3]);
assert!((ex - 1.0).abs() < 1e-5, "mirrored endpoint x: {ex}");
assert!(
(ey - 2.0).abs() < 1e-5,
"mirrored endpoint y should flip sign vs the non-mirrored case: {ey}"
);
assert!(
signed_area(&pl.points) < 0.0,
"a reflection must invert the winding, not merely move points; got {:?}",
pl.points
);
}
fn signed_area(points: &[f32]) -> f32 {
let n = points.len() / 2;
let mut sum = 0.0;
for i in 0..n {
let j = (i + 1) % n;
sum += points[2 * i] * points[2 * j + 1] - points[2 * j] * points[2 * i + 1];
}
sum
}
#[test]
fn pure_rotation_is_unchanged() {
let content = fixture("", "$").replace(
"#20=IFCDIRECTION((1.,0.));",
"#20=IFCDIRECTION((0.,1.));",
);
let data = extract_symbolic_data(&content);
let pl = data
.polylines
.iter()
.find(|p| p.representation == "Annotation")
.expect("mapped annotation polyline");
let (ex, ey) = (pl.points[2], pl.points[3]);
assert!((ex - -2.0).abs() < 1e-5, "rotated endpoint x: {ex}");
assert!((ey - -1.0).abs() < 1e-5, "rotated endpoint y: {ey}");
}
#[test]
fn uniform_scale_is_unchanged() {
let content = fixture("", "$").replace(
"#22=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#20,$,#21,$);",
"#22=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#20,$,#21,2.);",
);
let data = extract_symbolic_data(&content);
let pl = data
.polylines
.iter()
.find(|p| p.representation == "Annotation")
.expect("mapped annotation polyline");
let (ex, ey) = (pl.points[2], pl.points[3]);
assert!((ex - 2.0).abs() < 1e-5, "scaled endpoint x: {ex}");
assert!((ey - -4.0).abs() < 1e-5, "scaled endpoint y: {ey}");
}
#[test]
fn translation_is_unchanged() {
let content = fixture("", "$").replace(
"#21=IFCCARTESIANPOINT((0.,0.));",
"#21=IFCCARTESIANPOINT((5.,7.));",
);
let data = extract_symbolic_data(&content);
let pl = data
.polylines
.iter()
.find(|p| p.representation == "Annotation")
.expect("mapped annotation polyline");
let (sx, sy) = (pl.points[0], pl.points[1]);
let (ex, ey) = (pl.points[2], pl.points[3]);
assert!((sx - 5.0).abs() < 1e-5, "start x: {sx}");
assert!((sy - -7.0).abs() < 1e-5, "start y: {sy}");
assert!((ex - 6.0).abs() < 1e-5, "translated endpoint x: {ex}");
assert!((ey - -9.0).abs() < 1e-5, "translated endpoint y: {ey}");
}
#[test]
fn identity_mapping_target_lands_unmoved() {
let data = extract_symbolic_data(&fixture("", "$"));
let pl = data
.polylines
.iter()
.find(|p| p.representation == "Annotation")
.expect("mapped annotation polyline");
let (sx, sy) = (pl.points[0], pl.points[1]);
assert_eq!((sx, sy), (0.0, 0.0), "identity start point");
}