use ifc_lite_processing::{
process_geometry_streaming_filtered_with_options, MeshCoordinateSpace, MeshData,
OpeningFilterMode, StreamingOptions,
};
const SITE_T: (f64, f64, f64) = (10.0, 20.0, 0.0);
const SITE_ROTATION_30DEG: &str = "#31=IFCDIRECTION((0.,0.,1.));\n#32=IFCDIRECTION((0.866,0.5,0.));";
fn model(rotation: Option<&str>) -> String {
let (axis_refdir, site_placement) = match rotation {
Some(dirs) => (dirs, "#33=IFCAXIS2PLACEMENT3D(#30,#31,#32);"),
None => ("", "#33=IFCAXIS2PLACEMENT3D(#30,$,$);"),
};
format!(
r##"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('','2026-01-01T00:00:00',(''),(''),'test','test','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#2=IFCUNITASSIGNMENT((#1));
#3=IFCCARTESIANPOINT((0.,0.,0.));
#4=IFCAXIS2PLACEMENT3D(#3,$,$);
#5=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-06,#4,$);
#6=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#5,$,.MODEL_VIEW.,$);
#7=IFCPROJECT('11tEAnIV5BixApwp1YzpwS',$,'t',$,$,$,$,(#5),#2);
#30=IFCCARTESIANPOINT(({tx}.,{ty}.,{tz}.));
{axis_refdir}
{site_placement}
#34=IFCLOCALPLACEMENT($,#33);
#35=IFCSITE('1s1tEAnIV5BixApwp1Yzp0',$,'site',$,$,#34,$,$,.ELEMENT.,$,$,$,$,$);
#8=IFCCARTESIANPOINT((0.,0.));
#9=IFCCARTESIANPOINT((4.,0.));
#10=IFCCARTESIANPOINT((4.,1.));
#11=IFCCARTESIANPOINT((0.,1.));
#12=IFCPOLYLINE((#8,#9,#10,#11,#8));
#13=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#12);
#14=IFCCARTESIANPOINT((0.,0.,0.));
#15=IFCAXIS2PLACEMENT3D(#14,$,$);
#16=IFCDIRECTION((0.,0.,1.));
#17=IFCEXTRUDEDAREASOLID(#13,#15,#16,2.);
#18=IFCSHAPEREPRESENTATION(#6,'Body','SweptSolid',(#17));
#19=IFCCARTESIANPOINT((0.,0.,0.));
#20=IFCAXIS2PLACEMENT3D(#19,$,$);
#21=IFCREPRESENTATIONMAP(#20,#18);
#40=IFCCARTESIANPOINT((0.,0.,0.));
#41=IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#40,$,$);
#42=IFCMAPPEDITEM(#21,#41);
#43=IFCSHAPEREPRESENTATION(#6,'Body','MappedRepresentation',(#42));
#44=IFCPRODUCTDEFINITIONSHAPE($,$,(#43));
#45=IFCCARTESIANPOINT((0.,0.,0.));
#46=IFCAXIS2PLACEMENT3D(#45,$,$);
#47=IFCLOCALPLACEMENT(#34,#46);
#48=IFCBUILDINGELEMENTPROXY('36FTsOKg956eWgO6DwnT8U',$,'box1',$,$,#47,#44,$,$);
#50=IFCCARTESIANPOINT((0.,0.,0.));
#51=IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#50,$,$);
#52=IFCMAPPEDITEM(#21,#51);
#53=IFCSHAPEREPRESENTATION(#6,'Body','MappedRepresentation',(#52));
#54=IFCPRODUCTDEFINITIONSHAPE($,$,(#53));
#55=IFCCARTESIANPOINT((6.,0.,0.));
#56=IFCAXIS2PLACEMENT3D(#55,$,$);
#57=IFCLOCALPLACEMENT(#34,#56);
#58=IFCBUILDINGELEMENTPROXY('36FTsOKg956eWgO6DwnT8V',$,'box2',$,$,#57,#54,$,$);
ENDSEC;
END-ISO-10303-21;
"##,
tx = SITE_T.0,
ty = SITE_T.1,
tz = SITE_T.2,
)
}
fn run(rotation: Option<&str>) -> Vec<MeshData> {
let ifc = model(rotation);
let options = StreamingOptions {
initial_batch_size: usize::MAX,
throughput_batch_size: usize::MAX,
enable_instancing: true,
retain_emitted_meshes: true,
..StreamingOptions::default()
};
let result = process_geometry_streaming_filtered_with_options(
ifc.as_bytes(),
OpeningFilterMode::Default,
options,
|_, _, _| {},
|_| {},
|_| {},
);
assert_eq!(
result.mesh_coordinate_space,
MeshCoordinateSpace::SiteLocal,
"a translated IfcSite must select the site_local tier"
);
result.meshes
}
#[test]
fn translated_only_site_keeps_instancing_metadata() {
let meshes = run(None);
let proxy_meshes: Vec<&MeshData> = meshes
.iter()
.filter(|m| m.express_id == 48 || m.express_id == 58)
.collect();
assert!(
!proxy_meshes.is_empty(),
"expected meshes for the two box proxies (#48, #58)"
);
let carries_instance = proxy_meshes.iter().any(|m| m.instance.is_some());
assert!(
carries_instance,
"translated-only site_local model must retain instancing metadata \
on at least one occurrence of the shared representation map; \
got instance=None on every mesh: {:?}",
proxy_meshes
.iter()
.map(|m| (m.express_id, m.instance.is_some()))
.collect::<Vec<_>>()
);
let by_id: std::collections::BTreeMap<u32, bool> = proxy_meshes
.iter()
.map(|m| (m.express_id, m.instance.is_some()))
.collect();
assert_eq!(
by_id,
std::collections::BTreeMap::from([(48, true), (58, true)]),
"expected both box proxies (#48, #58) to appear in result.meshes, \
each carrying instance metadata; got: {:?}",
by_id
);
}
#[test]
fn rotated_site_keeps_instancing_metadata() {
let meshes = run(Some(SITE_ROTATION_30DEG));
let proxy_meshes: Vec<&MeshData> = meshes
.iter()
.filter(|m| m.express_id == 48 || m.express_id == 58)
.collect();
assert!(
!proxy_meshes.is_empty(),
"expected meshes for the two box proxies (#48, #58)"
);
let by_id: std::collections::BTreeMap<u32, bool> = proxy_meshes
.iter()
.map(|m| (m.express_id, m.instance.is_some()))
.collect();
assert_eq!(
by_id,
std::collections::BTreeMap::from([(48, true), (58, true)]),
"a 30-degree-yawed site must keep instancing metadata on BOTH box \
proxies (#48, #58) — the site rotation travels to the collator as a \
basis now, it does not cost the metadata; got: {:?}",
by_id
);
let reps: Vec<u128> = proxy_meshes
.iter()
.filter_map(|m| m.instance.as_ref().map(|i| i.rep_identity))
.collect();
assert_eq!(reps.len(), 2, "both occurrences must carry a rep_identity");
assert_eq!(
reps[0], reps[1],
"two occurrences of one IfcRepresentationMap must share a rep_identity, \
or nothing can ever group them"
);
assert!(
proxy_meshes
.iter()
.all(|m| m.instance.as_ref().is_some_and(|i| i.instanceable)),
"metadata that is not `instanceable` is skipped by the collator, so \
keeping it would buy nothing"
);
}