1use crate::prepass::{
29 flat_material_colors, flat_styles_rgba8, flat_voids, resolve_prepass, PrepassSpans,
30 ResolveOptions,
31};
32use crate::processor::{process_geometry_filtered_with_quality, OpeningFilterMode};
33use ifc_lite_core::{build_entity_index, EntityDecoder, EntityScanner};
34use ifc_lite_geometry::TessellationQuality;
35use serde::{Deserialize, Serialize};
36
37pub const FIXTURE_IFC: &str = r#"ISO-10303-21;
51HEADER;
52FILE_DESCRIPTION(('mesh-output determinism manifest fixture'),'2;1');
53FILE_NAME('mesh_determinism.ifc','2026-07-01T00:00:00',(''),(''),'','','');
54FILE_SCHEMA(('IFC4'));
55ENDSEC;
56DATA;
57#1=IFCPROJECT('0DeterminismProject00A',$,'Determinism',$,$,$,$,(#10),#7);
58#7=IFCUNITASSIGNMENT((#8));
59#8=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
60#10=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#11,$);
61#11=IFCAXIS2PLACEMENT3D(#12,$,$);
62#12=IFCCARTESIANPOINT((0.,0.,0.));
63#13=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#10,$,.MODEL_VIEW.,$);
64#20=IFCLOCALPLACEMENT($,#11);
65#30=IFCRECTANGLEPROFILEDEF(.AREA.,'WallProfile',#31,4.0,0.3);
66#31=IFCAXIS2PLACEMENT2D(#32,#33);
67#32=IFCCARTESIANPOINT((0.,0.));
68#33=IFCDIRECTION((1.,0.));
69#40=IFCEXTRUDEDAREASOLID(#30,#41,#42,2.5);
70#41=IFCAXIS2PLACEMENT3D(#12,$,$);
71#42=IFCDIRECTION((0.,0.,1.));
72#50=IFCSHAPEREPRESENTATION(#13,'Body','SweptSolid',(#40));
73#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
74#100=IFCWALL('0DeterminismWall0000A',$,'Wall',$,$,#20,#51,$,$);
75#110=IFCLOCALPLACEMENT(#20,#111);
76#111=IFCAXIS2PLACEMENT3D(#112,#113,#114);
77#112=IFCCARTESIANPOINT((0.,-0.5,1.25));
78#113=IFCDIRECTION((0.,1.,0.));
79#114=IFCDIRECTION((1.,0.,0.));
80#127=IFCRECTANGLEPROFILEDEF(.AREA.,'OpeningProfile',#128,1.2,1.5);
81#128=IFCAXIS2PLACEMENT2D(#32,#33);
82#131=IFCEXTRUDEDAREASOLID(#127,#132,#42,1.0);
83#132=IFCAXIS2PLACEMENT3D(#12,$,$);
84#140=IFCSHAPEREPRESENTATION(#13,'Body','SweptSolid',(#131));
85#141=IFCPRODUCTDEFINITIONSHAPE($,$,(#140));
86#200=IFCOPENINGELEMENT('0DeterminismOpening0A',$,'Opening',$,$,#110,#141,$,.OPENING.);
87#300=IFCRELVOIDSELEMENT('0DeterminismVoids000A',$,$,$,#100,#200);
88#400=IFCBUILDINGELEMENTPROXY('0DeterminismProxy000A',$,'MultiMaterial',$,$,#401,#402,$,$);
89#401=IFCLOCALPLACEMENT($,#403);
90#403=IFCAXIS2PLACEMENT3D(#404,$,$);
91#404=IFCCARTESIANPOINT((6.,0.,0.));
92#402=IFCPRODUCTDEFINITIONSHAPE($,$,(#405));
93#405=IFCSHAPEREPRESENTATION(#13,'Body','Tessellation',(#406));
94#406=IFCTRIANGULATEDFACESET(#407,$,.T.,((1,2,3),(1,2,4),(1,4,3),(2,3,4)),$);
95#407=IFCCARTESIANPOINTLIST3D(((0.,0.,0.),(1.,0.,0.),(0.,1.,0.),(0.,0.,1.)));
96#430=IFCSTYLEDITEM($,(#431),$);
97#431=IFCSURFACESTYLE('Glazing',.BOTH.,(#432));
98#432=IFCSURFACESTYLERENDERING(#433,0.5,$,$,$,$,$,$,.FLAT.);
99#433=IFCCOLOURRGB($,0.2,0.4,0.8);
100#434=IFCSTYLEDITEM($,(#435),$);
101#435=IFCSURFACESTYLE('Frame',.BOTH.,(#436));
102#436=IFCSURFACESTYLERENDERING(#437,$,$,$,$,$,$,$,.FLAT.);
103#437=IFCCOLOURRGB($,0.7,0.5,0.2);
104#440=IFCMATERIAL('Glazing',$,$);
105#441=IFCMATERIALDEFINITIONREPRESENTATION($,$,(#442),#440);
106#442=IFCSTYLEDREPRESENTATION(#10,'Style','Material',(#430));
107#445=IFCMATERIAL('Frame',$,$);
108#446=IFCMATERIALDEFINITIONREPRESENTATION($,$,(#447),#445);
109#447=IFCSTYLEDREPRESENTATION(#10,'Style','Material',(#434));
110#450=IFCMATERIALLIST((#440,#445));
111#460=IFCRELASSOCIATESMATERIAL('0DeterminismRelMat00A',$,$,$,(#400),#450);
112#500=IFCCOLUMN('0DeterminismColumn00A',$,'Column',$,$,#501,#502,$,$);
113#501=IFCLOCALPLACEMENT($,#503);
114#503=IFCAXIS2PLACEMENT3D(#504,$,$);
115#504=IFCCARTESIANPOINT((10.,0.,0.));
116#502=IFCPRODUCTDEFINITIONSHAPE($,$,(#505));
117#505=IFCSHAPEREPRESENTATION(#13,'Body','SweptSolid',(#506));
118#506=IFCEXTRUDEDAREASOLID(#507,#508,#42,3.0);
119#507=IFCCIRCLEPROFILEDEF(.AREA.,'ColumnProfile',#31,0.25);
120#508=IFCAXIS2PLACEMENT3D(#12,$,$);
121#530=IFCSTYLEDITEM(#506,(#531),$);
122#531=IFCSURFACESTYLE('Concrete',.BOTH.,(#532));
123#532=IFCSURFACESTYLERENDERING(#533,$,$,$,$,$,$,$,.FLAT.);
124#533=IFCCOLOURRGB($,0.62,0.6,0.55);
125#600=IFCWALL('0DeterminismWall0600A',$,'Wall2',$,$,#601,#651,$,$);
126#601=IFCLOCALPLACEMENT($,#602);
127#602=IFCAXIS2PLACEMENT3D(#603,$,$);
128#603=IFCCARTESIANPOINT((0.,3.,0.));
129#630=IFCRECTANGLEPROFILEDEF(.AREA.,'WallProfile2',#31,3.0,0.3);
130#640=IFCEXTRUDEDAREASOLID(#630,#41,#42,2.5);
131#650=IFCSHAPEREPRESENTATION(#13,'Body','SweptSolid',(#640));
132#651=IFCPRODUCTDEFINITIONSHAPE($,$,(#650));
133#700=IFCOPENINGELEMENT('0DeterminismOpening7A',$,'Opening2',$,$,#710,#741,$,.OPENING.);
134#710=IFCLOCALPLACEMENT(#601,#711);
135#711=IFCAXIS2PLACEMENT3D(#712,#113,#114);
136#712=IFCCARTESIANPOINT((0.5,-0.5,1.0));
137#727=IFCRECTANGLEPROFILEDEF(.AREA.,'OpeningProfile2',#128,0.9,1.2);
138#731=IFCEXTRUDEDAREASOLID(#727,#132,#42,1.0);
139#740=IFCSHAPEREPRESENTATION(#13,'Body','SweptSolid',(#731));
140#741=IFCPRODUCTDEFINITIONSHAPE($,$,(#740));
141#800=IFCRELVOIDSELEMENT('0DeterminismVoids800A',$,$,$,#600,#700);
142#820=IFCRELASSOCIATESMATERIAL('0DeterminismRelMat82A',$,$,$,(#500),#445);
143ENDSEC;
144END-ISO-10303-21;
145"#;
146
147const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
148const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
149
150fn fnv1a_bytes(h: &mut u64, bytes: &[u8]) {
151 for &b in bytes {
152 *h ^= b as u64;
153 *h = h.wrapping_mul(FNV_PRIME);
154 }
155}
156
157fn fnv1a_u32s(h: &mut u64, vals: &[u32]) {
158 for v in vals {
159 fnv1a_bytes(h, &v.to_le_bytes());
160 }
161}
162
163fn fnv1a_f32_bits(h: &mut u64, vals: &[f32]) {
164 for v in vals {
165 fnv1a_bytes(h, &v.to_bits().to_le_bytes());
166 }
167}
168
169fn hex(h: u64) -> String {
170 format!("0x{h:016x}")
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct MeshManifestEntry {
177 pub express_id: u32,
178 pub geometry_class: u8,
179 pub vertex_count: usize,
180 pub triangle_count: usize,
181 pub positions_hash: String,
186 pub normals_hash: String,
190 pub indices_origin_hash: String,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct MeshManifest {
198 pub hash: String,
202 pub mesh_count: usize,
203 pub vertex_count: usize,
204 pub triangle_count: usize,
205 pub voids_hash: String,
207 pub void_host_count: usize,
210 pub material_colors_hash: String,
212 pub material_element_count: usize,
214 pub styles_hash: String,
217 pub style_entry_count: usize,
220 pub meshes: Vec<MeshManifestEntry>,
221}
222
223impl MeshManifest {
224 pub fn to_json(&self) -> String {
225 let mut json = serde_json::to_string_pretty(self)
226 .expect("MeshManifest serialization cannot fail");
227 json.push('\n');
228 json
229 }
230
231 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
232 serde_json::from_str(json)
233 }
234}
235
236struct FixtureWires {
239 void_keys: Vec<u32>,
240 void_counts: Vec<u32>,
241 void_values: Vec<u32>,
242 mat_ids: Vec<u32>,
243 mat_counts: Vec<u32>,
244 mat_rgba: Vec<u8>,
245 style_ids: Vec<u32>,
246 style_rgba: Vec<u8>,
247}
248
249fn resolve_fixture_wires(content: &[u8]) -> FixtureWires {
254 let entity_index = std::sync::Arc::new(build_entity_index(content));
255 let mut decoder = EntityDecoder::with_arc_index(content, entity_index);
256 let mut spans = PrepassSpans::default();
257 let mut scanner = EntityScanner::new(content);
258 while let Some((id, type_name, start, end)) = scanner.next_entity() {
259 match type_name {
260 "IFCSTYLEDITEM" => spans.styled_items.push((id, start, end)),
261 "IFCINDEXEDCOLOURMAP" => spans.indexed_colour_maps.push((id, start, end)),
262 "IFCMATERIALDEFINITIONREPRESENTATION" => {
263 spans.material_def_reprs.push((id, start, end))
264 }
265 "IFCRELASSOCIATESMATERIAL" => spans.rel_associates_material.push((id, start, end)),
266 "IFCRELVOIDSELEMENT" => spans.void_rels.push((id, start, end)),
267 "IFCRELFILLSELEMENT" => spans.fills_rels.push((id, start, end)),
268 "IFCRELAGGREGATES" => spans.aggregate_rels.push((id, start, end)),
269 _ => {}
270 }
271 }
272 let resolved = resolve_prepass(&spans, &mut decoder, ResolveOptions::default());
273 let (void_keys, void_counts, void_values) = flat_voids(&resolved.void_index);
274 let (mat_ids, mat_counts, mat_rgba) = flat_material_colors(&resolved.element_material_colors);
275 let (style_ids, style_rgba) = flat_styles_rgba8(&resolved, &mut decoder);
276 FixtureWires {
277 void_keys,
278 void_counts,
279 void_values,
280 mat_ids,
281 mat_counts,
282 mat_rgba,
283 style_ids,
284 style_rgba,
285 }
286}
287
288pub fn compute_mesh_manifest() -> MeshManifest {
299 ifc_lite_geometry::local_frame_set_enabled_override(Some(true));
300
301 let result = process_geometry_filtered_with_quality(
302 FIXTURE_IFC,
303 OpeningFilterMode::Default,
304 TessellationQuality::Medium,
305 );
306
307 let wires = resolve_fixture_wires(FIXTURE_IFC.as_bytes());
308
309 let mut meshes = Vec::with_capacity(result.meshes.len());
310 let mut top = FNV_OFFSET_BASIS;
311 let mut vertex_count = 0usize;
312 let mut triangle_count = 0usize;
313 for mesh in &result.meshes {
314 let mut hp = FNV_OFFSET_BASIS;
316 fnv1a_f32_bits(&mut hp, &mesh.positions);
317 let mut hn = FNV_OFFSET_BASIS;
319 fnv1a_f32_bits(&mut hn, &mesh.normals);
320 let mut hio = FNV_OFFSET_BASIS;
322 fnv1a_bytes(&mut hio, &mesh.express_id.to_le_bytes());
323 fnv1a_bytes(&mut hio, &[mesh.geometry_class]);
324 fnv1a_u32s(&mut hio, &mesh.indices);
325 for c in mesh.origin {
326 fnv1a_bytes(&mut hio, &c.to_bits().to_le_bytes());
327 }
328 fnv1a_bytes(&mut top, &hp.to_le_bytes());
330 fnv1a_bytes(&mut top, &hn.to_le_bytes());
331 fnv1a_bytes(&mut top, &hio.to_le_bytes());
332 vertex_count += mesh.positions.len() / 3;
333 triangle_count += mesh.indices.len() / 3;
334 meshes.push(MeshManifestEntry {
335 express_id: mesh.express_id,
336 geometry_class: mesh.geometry_class,
337 vertex_count: mesh.positions.len() / 3,
338 triangle_count: mesh.indices.len() / 3,
339 positions_hash: hex(hp),
340 normals_hash: hex(hn),
341 indices_origin_hash: hex(hio),
342 });
343 }
344
345 let mut voids_hash = FNV_OFFSET_BASIS;
346 fnv1a_u32s(&mut voids_hash, &wires.void_keys);
347 fnv1a_u32s(&mut voids_hash, &wires.void_counts);
348 fnv1a_u32s(&mut voids_hash, &wires.void_values);
349
350 let mut mat_hash = FNV_OFFSET_BASIS;
351 fnv1a_u32s(&mut mat_hash, &wires.mat_ids);
352 fnv1a_u32s(&mut mat_hash, &wires.mat_counts);
353 fnv1a_bytes(&mut mat_hash, &wires.mat_rgba);
354
355 let mut styles_hash = FNV_OFFSET_BASIS;
356 fnv1a_u32s(&mut styles_hash, &wires.style_ids);
357 fnv1a_bytes(&mut styles_hash, &wires.style_rgba);
358
359 fnv1a_bytes(&mut top, b"voids");
360 fnv1a_bytes(&mut top, &voids_hash.to_le_bytes());
361 fnv1a_bytes(&mut top, b"material_colors");
362 fnv1a_bytes(&mut top, &mat_hash.to_le_bytes());
363 fnv1a_bytes(&mut top, b"styles");
364 fnv1a_bytes(&mut top, &styles_hash.to_le_bytes());
365
366 MeshManifest {
367 hash: hex(top),
368 mesh_count: result.meshes.len(),
369 vertex_count,
370 triangle_count,
371 voids_hash: hex(voids_hash),
372 void_host_count: wires.void_keys.len(),
373 material_colors_hash: hex(mat_hash),
374 material_element_count: wires.mat_ids.len(),
375 styles_hash: hex(styles_hash),
376 style_entry_count: wires.style_ids.len(),
377 meshes,
378 }
379}
380
381pub fn diff_report(expected: &MeshManifest, actual: &MeshManifest) -> Option<String> {
385 if expected == actual {
386 return None;
387 }
388 let mut lines = Vec::new();
389 if expected.hash != actual.hash {
390 lines.push(format!("hash: expected {} got {}", expected.hash, actual.hash));
391 }
392 if expected.mesh_count != actual.mesh_count {
393 lines.push(format!(
394 "mesh_count: expected {} got {}",
395 expected.mesh_count, actual.mesh_count
396 ));
397 }
398 if expected.vertex_count != actual.vertex_count {
399 lines.push(format!(
400 "vertex_count: expected {} got {}",
401 expected.vertex_count, actual.vertex_count
402 ));
403 }
404 if expected.triangle_count != actual.triangle_count {
405 lines.push(format!(
406 "triangle_count: expected {} got {}",
407 expected.triangle_count, actual.triangle_count
408 ));
409 }
410 if expected.voids_hash != actual.voids_hash {
411 lines.push(format!(
412 "voids_hash: expected {} got {}",
413 expected.voids_hash, actual.voids_hash
414 ));
415 }
416 if expected.void_host_count != actual.void_host_count {
417 lines.push(format!(
418 "void_host_count: expected {} got {}",
419 expected.void_host_count, actual.void_host_count
420 ));
421 }
422 if expected.material_colors_hash != actual.material_colors_hash {
423 lines.push(format!(
424 "material_colors_hash: expected {} got {}",
425 expected.material_colors_hash, actual.material_colors_hash
426 ));
427 }
428 if expected.material_element_count != actual.material_element_count {
429 lines.push(format!(
430 "material_element_count: expected {} got {}",
431 expected.material_element_count, actual.material_element_count
432 ));
433 }
434 if expected.styles_hash != actual.styles_hash {
435 lines.push(format!(
436 "styles_hash: expected {} got {}",
437 expected.styles_hash, actual.styles_hash
438 ));
439 }
440 if expected.style_entry_count != actual.style_entry_count {
441 lines.push(format!(
442 "style_entry_count: expected {} got {}",
443 expected.style_entry_count, actual.style_entry_count
444 ));
445 }
446 let fmt = |m: &MeshManifestEntry| {
447 format!(
448 "#{} class {} v{} t{} pos={} nrm={} io={}",
449 m.express_id,
450 m.geometry_class,
451 m.vertex_count,
452 m.triangle_count,
453 m.positions_hash,
454 m.normals_hash,
455 m.indices_origin_hash,
456 )
457 };
458 let common = expected.meshes.len().min(actual.meshes.len());
459 for i in 0..common {
460 let (e, a) = (&expected.meshes[i], &actual.meshes[i]);
461 if e != a {
462 let mut which = Vec::new();
463 if e.express_id != a.express_id {
464 which.push("express_id");
465 }
466 if e.geometry_class != a.geometry_class {
467 which.push("geometry_class");
468 }
469 if e.vertex_count != a.vertex_count {
470 which.push("vertex_count");
471 }
472 if e.triangle_count != a.triangle_count {
473 which.push("triangle_count");
474 }
475 if e.positions_hash != a.positions_hash {
476 which.push("positions");
477 }
478 if e.normals_hash != a.normals_hash {
479 which.push("normals");
480 }
481 if e.indices_origin_hash != a.indices_origin_hash {
482 which.push("indices/origin");
483 }
484 lines.push(format!(
485 "mesh[{i}]: differs in [{}]: expected {} got {}",
486 which.join("+"),
487 fmt(e),
488 fmt(a)
489 ));
490 }
491 }
492 for (i, e) in expected.meshes.iter().enumerate().skip(common) {
493 lines.push(format!("mesh[{i}]: expected {} got NOTHING", fmt(e)));
494 }
495 for (i, a) in actual.meshes.iter().enumerate().skip(common) {
496 lines.push(format!("mesh[{i}]: expected NOTHING got {}", fmt(a)));
497 }
498 Some(lines.join("\n"))
499}