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 spans.stash(type_name, id, start, end);
260 }
261 let resolved = resolve_prepass(&spans, &mut decoder, ResolveOptions::default());
262 let (void_keys, void_counts, void_values) = flat_voids(&resolved.void_index);
263 let (mat_ids, mat_counts, mat_rgba) = flat_material_colors(&resolved.element_material_colors);
264 let (style_ids, style_rgba) = flat_styles_rgba8(&resolved, &mut decoder);
265 FixtureWires {
266 void_keys,
267 void_counts,
268 void_values,
269 mat_ids,
270 mat_counts,
271 mat_rgba,
272 style_ids,
273 style_rgba,
274 }
275}
276
277pub fn compute_mesh_manifest() -> MeshManifest {
288 ifc_lite_geometry::local_frame_set_enabled_override(Some(true));
289
290 let result = process_geometry_filtered_with_quality(
291 FIXTURE_IFC,
292 OpeningFilterMode::Default,
293 TessellationQuality::Medium,
294 );
295
296 let wires = resolve_fixture_wires(FIXTURE_IFC.as_bytes());
297
298 let mut meshes = Vec::with_capacity(result.meshes.len());
299 let mut top = FNV_OFFSET_BASIS;
300 let mut vertex_count = 0usize;
301 let mut triangle_count = 0usize;
302 for mesh in &result.meshes {
303 let mut hp = FNV_OFFSET_BASIS;
305 fnv1a_f32_bits(&mut hp, &mesh.positions);
306 let mut hn = FNV_OFFSET_BASIS;
308 fnv1a_f32_bits(&mut hn, &mesh.normals);
309 let mut hio = FNV_OFFSET_BASIS;
311 fnv1a_bytes(&mut hio, &mesh.express_id.to_le_bytes());
312 fnv1a_bytes(&mut hio, &[mesh.geometry_class]);
313 fnv1a_u32s(&mut hio, &mesh.indices);
314 for c in mesh.origin {
315 fnv1a_bytes(&mut hio, &c.to_bits().to_le_bytes());
316 }
317 fnv1a_bytes(&mut top, &hp.to_le_bytes());
319 fnv1a_bytes(&mut top, &hn.to_le_bytes());
320 fnv1a_bytes(&mut top, &hio.to_le_bytes());
321 vertex_count += mesh.positions.len() / 3;
322 triangle_count += mesh.indices.len() / 3;
323 meshes.push(MeshManifestEntry {
324 express_id: mesh.express_id,
325 geometry_class: mesh.geometry_class,
326 vertex_count: mesh.positions.len() / 3,
327 triangle_count: mesh.indices.len() / 3,
328 positions_hash: hex(hp),
329 normals_hash: hex(hn),
330 indices_origin_hash: hex(hio),
331 });
332 }
333
334 let mut voids_hash = FNV_OFFSET_BASIS;
335 fnv1a_u32s(&mut voids_hash, &wires.void_keys);
336 fnv1a_u32s(&mut voids_hash, &wires.void_counts);
337 fnv1a_u32s(&mut voids_hash, &wires.void_values);
338
339 let mut mat_hash = FNV_OFFSET_BASIS;
340 fnv1a_u32s(&mut mat_hash, &wires.mat_ids);
341 fnv1a_u32s(&mut mat_hash, &wires.mat_counts);
342 fnv1a_bytes(&mut mat_hash, &wires.mat_rgba);
343
344 let mut styles_hash = FNV_OFFSET_BASIS;
345 fnv1a_u32s(&mut styles_hash, &wires.style_ids);
346 fnv1a_bytes(&mut styles_hash, &wires.style_rgba);
347
348 fnv1a_bytes(&mut top, b"voids");
349 fnv1a_bytes(&mut top, &voids_hash.to_le_bytes());
350 fnv1a_bytes(&mut top, b"material_colors");
351 fnv1a_bytes(&mut top, &mat_hash.to_le_bytes());
352 fnv1a_bytes(&mut top, b"styles");
353 fnv1a_bytes(&mut top, &styles_hash.to_le_bytes());
354
355 MeshManifest {
356 hash: hex(top),
357 mesh_count: result.meshes.len(),
358 vertex_count,
359 triangle_count,
360 voids_hash: hex(voids_hash),
361 void_host_count: wires.void_keys.len(),
362 material_colors_hash: hex(mat_hash),
363 material_element_count: wires.mat_ids.len(),
364 styles_hash: hex(styles_hash),
365 style_entry_count: wires.style_ids.len(),
366 meshes,
367 }
368}
369
370pub fn diff_report(expected: &MeshManifest, actual: &MeshManifest) -> Option<String> {
374 if expected == actual {
375 return None;
376 }
377 let mut lines = Vec::new();
378 if expected.hash != actual.hash {
379 lines.push(format!("hash: expected {} got {}", expected.hash, actual.hash));
380 }
381 if expected.mesh_count != actual.mesh_count {
382 lines.push(format!(
383 "mesh_count: expected {} got {}",
384 expected.mesh_count, actual.mesh_count
385 ));
386 }
387 if expected.vertex_count != actual.vertex_count {
388 lines.push(format!(
389 "vertex_count: expected {} got {}",
390 expected.vertex_count, actual.vertex_count
391 ));
392 }
393 if expected.triangle_count != actual.triangle_count {
394 lines.push(format!(
395 "triangle_count: expected {} got {}",
396 expected.triangle_count, actual.triangle_count
397 ));
398 }
399 if expected.voids_hash != actual.voids_hash {
400 lines.push(format!(
401 "voids_hash: expected {} got {}",
402 expected.voids_hash, actual.voids_hash
403 ));
404 }
405 if expected.void_host_count != actual.void_host_count {
406 lines.push(format!(
407 "void_host_count: expected {} got {}",
408 expected.void_host_count, actual.void_host_count
409 ));
410 }
411 if expected.material_colors_hash != actual.material_colors_hash {
412 lines.push(format!(
413 "material_colors_hash: expected {} got {}",
414 expected.material_colors_hash, actual.material_colors_hash
415 ));
416 }
417 if expected.material_element_count != actual.material_element_count {
418 lines.push(format!(
419 "material_element_count: expected {} got {}",
420 expected.material_element_count, actual.material_element_count
421 ));
422 }
423 if expected.styles_hash != actual.styles_hash {
424 lines.push(format!(
425 "styles_hash: expected {} got {}",
426 expected.styles_hash, actual.styles_hash
427 ));
428 }
429 if expected.style_entry_count != actual.style_entry_count {
430 lines.push(format!(
431 "style_entry_count: expected {} got {}",
432 expected.style_entry_count, actual.style_entry_count
433 ));
434 }
435 let fmt = |m: &MeshManifestEntry| {
436 format!(
437 "#{} class {} v{} t{} pos={} nrm={} io={}",
438 m.express_id,
439 m.geometry_class,
440 m.vertex_count,
441 m.triangle_count,
442 m.positions_hash,
443 m.normals_hash,
444 m.indices_origin_hash,
445 )
446 };
447 let common = expected.meshes.len().min(actual.meshes.len());
448 for i in 0..common {
449 let (e, a) = (&expected.meshes[i], &actual.meshes[i]);
450 if e != a {
451 let mut which = Vec::new();
452 if e.express_id != a.express_id {
453 which.push("express_id");
454 }
455 if e.geometry_class != a.geometry_class {
456 which.push("geometry_class");
457 }
458 if e.vertex_count != a.vertex_count {
459 which.push("vertex_count");
460 }
461 if e.triangle_count != a.triangle_count {
462 which.push("triangle_count");
463 }
464 if e.positions_hash != a.positions_hash {
465 which.push("positions");
466 }
467 if e.normals_hash != a.normals_hash {
468 which.push("normals");
469 }
470 if e.indices_origin_hash != a.indices_origin_hash {
471 which.push("indices/origin");
472 }
473 lines.push(format!(
474 "mesh[{i}]: differs in [{}]: expected {} got {}",
475 which.join("+"),
476 fmt(e),
477 fmt(a)
478 ));
479 }
480 }
481 for (i, e) in expected.meshes.iter().enumerate().skip(common) {
482 lines.push(format!("mesh[{i}]: expected {} got NOTHING", fmt(e)));
483 }
484 for (i, a) in actual.meshes.iter().enumerate().skip(common) {
485 lines.push(format!("mesh[{i}]: expected NOTHING got {}", fmt(a)));
486 }
487 Some(lines.join("\n"))
488}