1use crate::style::{FullIndexedColourMap, GeometryStyleInfo};
38use crate::types::mesh::{MeshData, MeshTextureData};
39use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
40use ifc_lite_geometry::{
41 calculate_normals, BoolFailure, GeometryHasher, GeometryRouter, Mesh, ResolvedTextureMap,
42 SubMeshCollection,
43};
44use rustc_hash::{FxHashMap, FxHashSet};
45use std::collections::BTreeMap;
46
47use crate::processor::{convert_mesh_to_site_local, get_refs_from_list};
48
49#[derive(Debug, Clone, Default)]
53pub struct ElementMeshMetadata {
54 pub global_id: Option<String>,
55 pub name: Option<String>,
56 pub presentation_layer: Option<String>,
57 pub space_zone_properties: Option<BTreeMap<String, String>>,
58}
59
60#[derive(Debug, Clone)]
62pub enum ElementJobKind {
63 Product,
65 TypeProduct { rep_maps: Vec<(u32, u8)> },
70}
71
72pub struct ElementMeshJob<'a> {
74 pub id: u32,
75 pub ifc_type: IfcType,
76 pub entity: &'a DecodedEntity,
79 pub kind: ElementJobKind,
80 pub element_color: Option<[f32; 4]>,
83 pub metadata: Option<&'a ElementMeshMetadata>,
84}
85
86pub struct MeshProductionContext<'a> {
90 pub void_index: &'a FxHashMap<u32, Vec<u32>>,
92 pub geometry_style_index: &'a FxHashMap<u32, GeometryStyleInfo>,
94 pub indexed_colour_full: &'a FxHashMap<u32, FullIndexedColourMap>,
96 pub element_material_colors: &'a FxHashMap<u32, Vec<[f32; 4]>>,
99 pub texture_index: &'a FxHashMap<u32, ResolvedTextureMap>,
101 pub site_local_rotation: Option<&'a Vec<f64>>,
105}
106
107#[derive(Debug, Clone, Copy)]
109pub struct GeometryHashConfig {
110 pub tolerance: f64,
112 pub world_rtc: [f64; 3],
116}
117
118#[derive(Debug, Clone, Copy, Default)]
119pub struct MeshProductionOptions {
120 pub geometry_hash: Option<GeometryHashConfig>,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum TypeGeometryMode {
130 SuppressInstanced,
133 EmitTagged,
136}
137
138pub fn plan_type_geometry(
148 rep_map_ids: &[u32],
149 referenced_representation_maps: &FxHashSet<u32>,
150 type_is_instantiated: bool,
151 mode: TypeGeometryMode,
152) -> Vec<(u32, u8)> {
153 if mode == TypeGeometryMode::SuppressInstanced && type_is_instantiated {
154 return Vec::new();
155 }
156 let class: u8 = if type_is_instantiated { 2 } else { 1 };
157 rep_map_ids
158 .iter()
159 .filter(|rm| !referenced_representation_maps.contains(rm))
160 .map(|rm| (*rm, class))
161 .collect()
162}
163
164pub struct ProducedElementMeshes {
166 pub meshes: Vec<MeshData>,
167 pub geometry_hash: Option<u64>,
172 pub csg_failures: FxHashMap<u32, Vec<BoolFailure>>,
179}
180
181pub fn produce_element_meshes(
190 job: &ElementMeshJob<'_>,
191 ctx: &MeshProductionContext<'_>,
192 opts: &MeshProductionOptions,
193 decoder: &mut EntityDecoder,
194 router: &GeometryRouter,
195) -> ProducedElementMeshes {
196 let mut hasher = match (&job.kind, opts.geometry_hash) {
197 (ElementJobKind::Product, Some(cfg)) => {
198 Some(GeometryHasher::new(cfg.tolerance, cfg.world_rtc))
199 }
200 _ => None,
201 };
202
203 let meshes = produce_inner(job, ctx, decoder, router, &mut hasher);
204
205 let csg_failures = router.take_csg_failures();
208
209 let geometry_hash = hasher.and_then(|h| if h.is_empty() { None } else { Some(h.finish()) });
210
211 ProducedElementMeshes {
212 meshes,
213 geometry_hash,
214 csg_failures,
215 }
216}
217
218fn produce_inner(
219 job: &ElementMeshJob<'_>,
220 ctx: &MeshProductionContext<'_>,
221 decoder: &mut EntityDecoder,
222 router: &GeometryRouter,
223 hasher: &mut Option<GeometryHasher>,
224) -> Vec<MeshData> {
225 let has_representation = job.entity.get(6).is_some_and(|a| !a.is_null());
229 if !has_representation && job.ifc_type != IfcType::IfcAlignment {
230 return Vec::new();
231 }
232
233 let element_color = job
234 .element_color
235 .unwrap_or_else(|| crate::style::default_color_for_type(job.ifc_type).to_array());
236
237 if let ElementJobKind::TypeProduct { rep_maps } = &job.kind {
238 return produce_type_geometry(job, rep_maps, element_color, ctx, decoder, router);
239 }
240
241 let has_openings = ctx
242 .void_index
243 .get(&job.id)
244 .is_some_and(|openings| !openings.is_empty());
245
246 if has_openings {
247 if let Ok(sub_meshes) =
251 router.process_element_with_submeshes_and_voids(job.entity, decoder, ctx.void_index)
252 {
253 if !sub_meshes.is_empty() {
254 let out = emit_sub_meshes(job, sub_meshes, element_color, ctx, decoder, hasher);
255 if !out.is_empty() {
256 return out;
257 }
258 }
259 }
260 } else {
261 if let Ok(sub_meshes) = router.process_element_with_submeshes(job.entity, decoder) {
267 if !sub_meshes.is_empty() {
268 let out = emit_sub_meshes(job, sub_meshes, element_color, ctx, decoder, hasher);
269 if !out.is_empty() {
270 return out;
271 }
272 }
273 }
274 }
275
276 let _ = router.take_csg_failures();
283
284 let mut mesh_candidate = router
285 .process_element_with_voids(job.entity, decoder, ctx.void_index)
286 .ok();
287 let needs_fallback = match mesh_candidate.as_ref() {
288 Some(mesh) => mesh.is_empty(),
289 None => true,
290 };
291 if needs_fallback {
292 mesh_candidate = router.process_element(job.entity, decoder).ok();
293 }
294
295 let Some(mut mesh) = mesh_candidate else {
296 return Vec::new();
297 };
298 if mesh.is_empty() {
299 return Vec::new();
300 }
301
302 if !ctx.indexed_colour_full.is_empty() {
308 if let Some(full) =
309 find_indexed_colour_for_element(job.entity, ctx.indexed_colour_full, decoder)
310 {
311 let geometry_id = full.geometry_id;
312 if let Some(groups) = crate::style::split_mesh_by_indexed_colour(&mesh, full) {
313 if let Some(h) = hasher.as_mut() {
314 h.add_mesh_with_origin(&mesh.positions, &mesh.indices, mesh.origin);
315 }
316 let mut out: Vec<MeshData> = Vec::with_capacity(groups.len());
317 for (color, mut part) in groups {
318 if part.normals.len() != part.positions.len() {
319 calculate_normals(&mut part);
320 }
321 out.push(build_mesh_data(
322 job,
323 part,
324 color.to_array(),
325 None,
326 Some(geometry_id),
327 0,
328 ctx,
329 ));
330 }
331 if !out.is_empty() {
332 return out;
333 }
334 }
335 }
336 }
337
338 if mesh.normals.len() != mesh.positions.len() {
339 calculate_normals(&mut mesh);
340 }
341 if let Some(h) = hasher.as_mut() {
342 h.add_mesh_with_origin(&mesh.positions, &mesh.indices, mesh.origin);
343 }
344 vec![build_mesh_data(job, mesh, element_color, None, None, 0, ctx)]
345}
346
347fn emit_sub_meshes(
351 job: &ElementMeshJob<'_>,
352 sub_meshes: SubMeshCollection,
353 element_color: [f32; 4],
354 ctx: &MeshProductionContext<'_>,
355 decoder: &mut EntityDecoder,
356 hasher: &mut Option<GeometryHasher>,
357) -> Vec<MeshData> {
358 let mut out: Vec<MeshData> = Vec::with_capacity(sub_meshes.len());
359 let material_colors = ctx.element_material_colors.get(&job.id);
363 let mut mat_color_idx = 0usize;
364
365 for sub in sub_meshes.sub_meshes {
366 let mut sub_mesh = sub.mesh;
367 if sub_mesh.is_empty() {
368 continue;
369 }
370 if sub_mesh.normals.len() != sub_mesh.positions.len() {
371 calculate_normals(&mut sub_mesh);
372 }
373
374 let style = ctx.geometry_style_index.get(&sub.geometry_id);
375 let direct_color = style.map(|s| s.color).or_else(|| {
378 find_geometry_item_color(sub.geometry_id, ctx.geometry_style_index, decoder)
379 });
380 let color = crate::style::resolve_submesh_color(
381 direct_color,
382 material_colors.map(|v| v.as_slice()),
383 &mut mat_color_idx,
384 element_color,
385 );
386 let material_name = style
387 .and_then(|s| s.material_name.as_ref())
388 .map(ToString::to_string)
389 .or_else(|| infer_opening_subpart_material_name(&job.ifc_type, color, sub.geometry_id));
390
391 if let Some(h) = hasher.as_mut() {
392 h.add_mesh_with_origin(&sub_mesh.positions, &sub_mesh.indices, sub_mesh.origin);
393 }
394
395 if let Some(full) = ctx.indexed_colour_full.get(&sub.geometry_id) {
400 if let Some(groups) = crate::style::split_mesh_by_indexed_colour(&sub_mesh, full) {
401 for (rgba, mut part) in groups {
402 if part.normals.len() != part.positions.len() {
403 calculate_normals(&mut part);
404 }
405 out.push(build_mesh_data(
406 job,
407 part,
408 rgba.to_array(),
409 None,
410 Some(sub.geometry_id),
411 0,
412 ctx,
413 ));
414 }
415 continue;
416 }
417 }
418
419 out.push(build_mesh_data(
420 job,
421 sub_mesh,
422 color,
423 material_name,
424 Some(sub.geometry_id),
425 0,
426 ctx,
427 ));
428 }
429 out
430}
431
432fn produce_type_geometry(
435 job: &ElementMeshJob<'_>,
436 rep_maps: &[(u32, u8)],
437 element_color: [f32; 4],
438 ctx: &MeshProductionContext<'_>,
439 decoder: &mut EntityDecoder,
440 router: &GeometryRouter,
441) -> Vec<MeshData> {
442 let mut out: Vec<MeshData> = Vec::new();
443 for &(rep_map_id, geometry_class) in rep_maps {
444 let Ok(rep_map) = decoder.decode_by_id(rep_map_id) else {
445 continue;
446 };
447 let Ok(parts) =
450 router.process_representation_map_with_texture(&rep_map, decoder, ctx.texture_index)
451 else {
452 continue;
453 };
454 if parts.is_empty() {
455 continue;
456 }
457
458 let color =
459 resolve_color_for_representation_map(rep_map_id, ctx.geometry_style_index, decoder)
460 .unwrap_or(element_color);
461
462 for (mut mesh, uvs, texture) in parts {
463 if mesh.is_empty() {
464 continue;
465 }
466 if mesh.normals.len() != mesh.positions.len() {
467 calculate_normals(&mut mesh);
468 }
469 let mut mesh_data =
470 build_mesh_data(job, mesh, color, None, None, geometry_class, ctx);
471 if let Some(tex) = texture {
472 mesh_data = mesh_data.with_texture(
473 uvs,
474 MeshTextureData {
475 rgba: tex.rgba,
476 width: tex.width,
477 height: tex.height,
478 repeat_s: tex.repeat_s,
479 repeat_t: tex.repeat_t,
480 },
481 );
482 }
483 out.push(mesh_data);
484 }
485 }
486 out
487}
488
489fn degenerate_backstop_disabled() -> bool {
495 static DISABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
496 *DISABLED.get_or_init(|| std::env::var("IFC_LITE_DISABLE_DEGENERATE_BACKSTOP").is_ok())
497}
498
499fn build_mesh_data(
503 job: &ElementMeshJob<'_>,
504 mut mesh: Mesh,
505 color: [f32; 4],
506 material_name: Option<String>,
507 geometry_item_id: Option<u32>,
508 geometry_class: u8,
509 ctx: &MeshProductionContext<'_>,
510) -> MeshData {
511 if !degenerate_backstop_disabled() {
520 mesh.drop_degenerate_triangles();
521 }
522 let mesh_origin = mesh.origin;
523 let mut mesh_data = MeshData::new(
524 job.id,
525 job.ifc_type.name().to_string(),
526 mesh.positions,
527 mesh.normals,
528 mesh.indices,
529 color,
530 )
531 .with_origin(mesh_origin);
532 if let Some(meta) = job.metadata {
533 mesh_data = mesh_data
534 .with_element_metadata(
535 meta.global_id.clone(),
536 meta.name.clone(),
537 meta.presentation_layer.clone(),
538 )
539 .with_properties(meta.space_zone_properties.clone());
540 }
541 if material_name.is_some() || geometry_item_id.is_some() {
542 mesh_data = mesh_data.with_style_metadata(material_name, geometry_item_id);
543 }
544 if geometry_class != 0 {
545 mesh_data = mesh_data.with_geometry_class(geometry_class);
546 }
547 convert_mesh_to_site_local(&mut mesh_data, ctx.site_local_rotation);
548 mesh_data
549}
550
551pub(crate) fn find_geometry_item_color(
556 geometry_id: u32,
557 geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
558 decoder: &mut EntityDecoder,
559) -> Option<[f32; 4]> {
560 if let Some(style) = geometry_styles.get(&geometry_id) {
562 return Some(style.color);
563 }
564
565 let geom = decoder.decode_by_id(geometry_id).ok()?;
568 if geom.ifc_type != IfcType::IfcMappedItem {
569 return None;
570 }
571 let mapping_source_id = geom.get_ref(0)?;
573 let representation_map = decoder.decode_by_id(mapping_source_id).ok()?;
575 let mapped_representation_id = representation_map.get_ref(1)?;
576 let mapped_representation = decoder.decode_by_id(mapped_representation_id).ok()?;
577 let items = get_refs_from_list(&mapped_representation, 3)?;
579 for underlying in items {
580 if let Some(color) = find_geometry_item_color(underlying, geometry_styles, decoder) {
581 return Some(color);
582 }
583 }
584 None
585}
586
587pub(crate) fn resolve_color_for_representation_map(
592 rep_map_id: u32,
593 geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
594 decoder: &mut EntityDecoder,
595) -> Option<[f32; 4]> {
596 let rep_map = decoder.decode_by_id(rep_map_id).ok()?;
597 let mapped_rep_id = rep_map.get_ref(1)?;
599 let mapped_rep = decoder.decode_by_id(mapped_rep_id).ok()?;
600 let item_ids = get_refs_from_list(&mapped_rep, 3)?;
602 for item_id in item_ids {
603 if let Some(style) = geometry_style_index.get(&item_id) {
604 return Some(style.color);
605 }
606 if let Some(color) = find_geometry_item_color(item_id, geometry_style_index, decoder) {
607 return Some(color);
608 }
609 }
610 None
611}
612
613pub(crate) fn find_indexed_colour_for_element<'a>(
617 entity: &DecodedEntity,
618 indexed_colour_full: &'a FxHashMap<u32, FullIndexedColourMap>,
619 decoder: &mut EntityDecoder,
620) -> Option<&'a FullIndexedColourMap> {
621 let pds_id = entity.get_ref(6)?;
622 let pds = decoder.decode_by_id(pds_id).ok()?;
623 let repr_ids = get_refs_from_list(&pds, 2)?;
624 for repr_id in repr_ids {
625 if let Ok(repr) = decoder.decode_by_id(repr_id) {
626 if let Some(items) = get_refs_from_list(&repr, 3) {
627 for item_id in items {
628 if let Some(full) = indexed_colour_full.get(&item_id) {
629 return Some(full);
630 }
631 }
632 }
633 }
634 }
635 None
636}
637
638fn is_opening_with_subparts(ifc_type: &IfcType) -> bool {
639 matches!(ifc_type, IfcType::IfcWindow | IfcType::IfcDoor)
640}
641
642pub(crate) fn infer_opening_subpart_material_name(
646 ifc_type: &IfcType,
647 color: [f32; 4],
648 geometry_id: u32,
649) -> Option<String> {
650 if !is_opening_with_subparts(ifc_type) {
651 return None;
652 }
653
654 let prefix = match ifc_type {
655 IfcType::IfcDoor => "Door",
656 _ => "Window",
657 };
658
659 if color[3] <= 0.65 {
660 return Some(format!("{}_Glass", prefix));
661 }
662
663 Some(format!("{}_Frame_{}", prefix, geometry_id))
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 fn refs(ids: &[u32]) -> FxHashSet<u32> {
671 ids.iter().copied().collect()
672 }
673
674 #[test]
675 fn plan_type_geometry_orphan_type_emits_unreferenced_maps_as_class_1() {
676 for mode in [TypeGeometryMode::SuppressInstanced, TypeGeometryMode::EmitTagged] {
677 let planned = plan_type_geometry(&[10, 11, 12], &refs(&[11]), false, mode);
678 assert_eq!(
679 planned,
680 vec![(10, 1), (12, 1)],
681 "orphan type: unreferenced maps render as class 1 in {mode:?}",
682 );
683 }
684 }
685
686 #[test]
687 fn plan_type_geometry_instantiated_type_suppressed_for_export_tagged_for_viewer() {
688 let suppress = plan_type_geometry(
689 &[10, 11],
690 &refs(&[]),
691 true,
692 TypeGeometryMode::SuppressInstanced,
693 );
694 assert!(
695 suppress.is_empty(),
696 "an export must never duplicate an instanced type's geometry"
697 );
698
699 let tagged =
700 plan_type_geometry(&[10, 11], &refs(&[]), true, TypeGeometryMode::EmitTagged);
701 assert_eq!(
702 tagged,
703 vec![(10, 2), (11, 2)],
704 "the viewer renders instanced type maps tagged class 2 for the Types view"
705 );
706 }
707
708 #[test]
709 fn plan_type_geometry_referenced_maps_never_emit() {
710 let planned = plan_type_geometry(
711 &[10],
712 &refs(&[10]),
713 false,
714 TypeGeometryMode::EmitTagged,
715 );
716 assert!(
717 planned.is_empty(),
718 "a map an IfcMappedItem instantiates draws through its occurrence"
719 );
720 }
721
722 #[test]
723 fn find_geometry_item_color_follows_mapped_item() {
724 const IFC: &str = r#"ISO-10303-21;
729HEADER;
730FILE_DESCRIPTION((''),'2;1');
731FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');
732FILE_SCHEMA(('IFC4'));
733ENDSEC;
734DATA;
735#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);
736#100=IFCMAPPEDITEM(#101,#105);
737#101=IFCREPRESENTATIONMAP(#102,#103);
738#102=IFCAXIS2PLACEMENT3D(#104,$,$);
739#103=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#110));
740#104=IFCCARTESIANPOINT((0.,0.,0.));
741#105=IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#104,$,$);
742ENDSEC;
743END-ISO-10303-21;
744"#;
745 let blue = [0.1, 0.2, 0.9, 1.0];
746 let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
747 styles.insert(110, GeometryStyleInfo::from_color(blue));
748
749 let mut decoder = EntityDecoder::new(IFC);
750
751 assert_eq!(find_geometry_item_color(100, &styles, &mut decoder), Some(blue));
753 assert_eq!(find_geometry_item_color(110, &styles, &mut decoder), Some(blue));
755 assert_eq!(find_geometry_item_color(101, &styles, &mut decoder), None);
757 }
758
759 #[test]
760 fn infer_opening_material_names_glass_vs_frame() {
761 let glass =
762 infer_opening_subpart_material_name(&IfcType::IfcWindow, [0.7, 0.9, 0.5, 0.3], 42);
763 assert_eq!(glass.as_deref(), Some("Window_Glass"));
764
765 let frame =
766 infer_opening_subpart_material_name(&IfcType::IfcDoor, [0.5, 0.5, 0.5, 1.0], 7);
767 assert_eq!(frame.as_deref(), Some("Door_Frame_7"));
768
769 let none = infer_opening_subpart_material_name(&IfcType::IfcWall, [1.0; 4], 1);
770 assert!(none.is_none(), "only windows/doors get inferred part names");
771 }
772}