use ifc_lite_geometry::{collate_refs, local_frame_set_enabled_override, InstanceMeshRef};
use ifc_lite_processing::{process_geometry, MeshData};
use std::sync::Mutex;
const QUAD_X_OFFSET: f64 = 1e-6;
const PLACEMENT_X: [f64; 3] = [0.0, 64.0, 128.0];
const RAW_VERTICES: usize = 12;
const WELDED_VERTICES: usize = 8;
const TRIANGLES: usize = 4;
fn fixture() -> String {
let mut lines: Vec<String> = Vec::new();
let mut next_id = 100u32;
let mut emit = |body: String, lines: &mut Vec<String>| {
let id = next_id;
next_id += 1;
lines.push(format!("#{id}={body};"));
id
};
let mut triangle = |corners: [(f64, f64); 3], dx: f64, lines: &mut Vec<String>| {
let points: Vec<u32> = corners
.iter()
.map(|(x, y)| emit(format!("IFCCARTESIANPOINT(({:?},{y:?},0.))", x + dx), lines))
.collect();
let refs: Vec<String> = points.iter().map(|p| format!("#{p}")).collect();
let loop_id = emit(format!("IFCPOLYLOOP(({}))", refs.join(",")), lines);
let bound = emit(format!("IFCFACEOUTERBOUND(#{loop_id},.T.)"), lines);
emit(format!("IFCFACE((#{bound}))"), lines)
};
let mut faces: Vec<u32> = Vec::new();
for dx in [0.0, QUAD_X_OFFSET] {
faces.push(triangle([(0., 0.), (1., 0.), (0., 1.)], dx, &mut lines));
faces.push(triangle([(1., 0.), (1., 1.), (0., 1.)], dx, &mut lines));
}
let face_refs: Vec<String> = faces.iter().map(|f| format!("#{f}")).collect();
let shell = emit(
format!("IFCOPENSHELL(({}))", face_refs.join(",")),
&mut lines,
);
let surface = emit(format!("IFCSHELLBASEDSURFACEMODEL((#{shell}))"), &mut lines);
let map_rep = emit(
format!("IFCSHAPEREPRESENTATION(#6,'Body','SurfaceModel',(#{surface}))"),
&mut lines,
);
let map_origin = emit("IFCAXIS2PLACEMENT3D(#3,$,$)".to_string(), &mut lines);
let rep_map = emit(
format!("IFCREPRESENTATIONMAP(#{map_origin},#{map_rep})"),
&mut lines,
);
let target = emit(
"IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#3,1.,$)".to_string(),
&mut lines,
);
for (k, x) in PLACEMENT_X.iter().enumerate() {
let point = emit(format!("IFCCARTESIANPOINT(({x:?},0.,0.))"), &mut lines);
let axis = emit(format!("IFCAXIS2PLACEMENT3D(#{point},$,$)"), &mut lines);
let placement = emit(format!("IFCLOCALPLACEMENT($,#{axis})"), &mut lines);
let item = emit(format!("IFCMAPPEDITEM(#{rep_map},#{target})"), &mut lines);
let shape = emit(
format!("IFCSHAPEREPRESENTATION(#6,'Body','MappedRepresentation',(#{item}))"),
&mut lines,
);
let definition = emit(
format!("IFCPRODUCTDEFINITIONSHAPE($,$,(#{shape}))"),
&mut lines,
);
emit(
format!(
"IFCFURNISHINGELEMENT('2Ab{k}cdefghijklmnopqrst',$,'seat{k}',$,$,#{placement},#{definition},$)"
),
&mut lines,
);
}
format!("{HEADER}{}\nENDSEC;\nEND-ISO-10303-21;\n", lines.join("\n"))
}
const HEADER: &str = 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);
"##;
fn meshes_in_native_frame() -> Vec<MeshData> {
static FRAME_OVERRIDE: Mutex<()> = Mutex::new(());
let _guard = FRAME_OVERRIDE.lock().unwrap_or_else(|e| e.into_inner());
local_frame_set_enabled_override(Some(false));
let result = process_geometry(fixture().as_bytes());
local_frame_set_enabled_override(None);
result.meshes
}
#[test]
fn occurrences_of_one_representation_map_share_one_buffer_shape() {
let meshes = meshes_in_native_frame();
assert_eq!(
meshes.len(),
PLACEMENT_X.len(),
"expected one mesh per mapped occurrence"
);
let triangles: Vec<usize> = meshes.iter().map(|m| m.indices.len() / 3).collect();
assert_eq!(
triangles,
vec![TRIANGLES; PLACEMENT_X.len()],
"two quads triangulate to {TRIANGLES} triangles at every placement"
);
let vertices: Vec<usize> = meshes.iter().map(|m| m.positions.len() / 3).collect();
assert_eq!(
vertices,
vec![WELDED_VERTICES; PLACEMENT_X.len()],
"every occurrence of one IfcRepresentationMap must weld identically, \
whatever its placement. {RAW_VERTICES} means nothing welded at all; \
fewer than {WELDED_VERTICES} at the far placement means the weld ran on \
baked world coordinates and merged the two quads (got {vertices:?})"
);
let template = &meshes[0];
for (k, mesh) in meshes.iter().enumerate() {
assert_eq!(
mesh.indices, template.indices,
"occurrence {k} must share the template's index buffer"
);
assert_eq!(
mesh.normals, template.normals,
"occurrence {k} must share the template's normals"
);
}
let far = meshes.last().expect("three occurrences");
let mut coincident = 0usize;
for a in 0..WELDED_VERTICES {
for b in (a + 1)..WELDED_VERTICES {
let same_position = (0..3).all(|k| far.positions[a * 3 + k] == far.positions[b * 3 + k]);
let same_normal = (0..3).all(|k| far.normals[a * 3 + k] == far.normals[b * 3 + k]);
if same_position && same_normal {
coincident += 1;
}
}
}
assert_eq!(
coincident, 4,
"at x={} the two quads' four corner pairs must share f32 world positions \
AND normals and still be separate vertices; {coincident} such pairs \
survived, so the weld is still keying on baked coordinates",
PLACEMENT_X[PLACEMENT_X.len() - 1],
);
}
#[test]
fn the_shared_map_collates_into_one_template() {
let meshes = meshes_in_native_frame();
let refs: Vec<InstanceMeshRef> = meshes
.iter()
.map(|m| InstanceMeshRef {
positions: &m.positions,
normals: &m.normals,
indices: &m.indices,
origin: m.origin,
instance_meta: m.instance.as_ref(),
entity_id: m.express_id,
color: m.color,
item_id: None,
})
.collect();
let collated = collate_refs(&refs, 2, [0.0, 0.0, 0.0]);
assert_eq!(
collated.templates.len(),
1,
"three occurrences of one representation map are one template"
);
assert_eq!(
collated.templates[0].occurrences.len(),
PLACEMENT_X.len(),
"every occurrence must be instanced against that template"
);
assert!(
collated.flat_indices.is_empty(),
"no occurrence should fall back to the flat path: {:?}",
collated.flat_indices
);
assert_eq!(
collated.verification_rejections, 0,
"the group must survive the #3666 reconstruction check"
);
}