Skip to main content

ifc_lite_geometry/router/
processing.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Core element processing: resolving representations, processing items, and caching.
6
7use super::GeometryRouter;
8use crate::{Error, Mesh, Result, SubMeshCollection};
9use ifc_lite_core::{DecodedEntity, EntityDecoder, GeometryCategory, IfcType};
10use nalgebra::Matrix4;
11use std::sync::Arc;
12
13impl GeometryRouter {
14    /// Compute median-based RTC offset from sampled translations.
15    /// Returns `(0,0,0)` if empty or coordinates are within 10km of origin.
16    fn rtc_offset_from_translations(translations: &[(f64, f64, f64)]) -> (f64, f64, f64) {
17        if translations.is_empty() {
18            return (0.0, 0.0, 0.0);
19        }
20
21        let mut x: Vec<f64> = translations.iter().map(|(x, _, _)| *x).collect();
22        let mut y: Vec<f64> = translations.iter().map(|(_, y, _)| *y).collect();
23        let mut z: Vec<f64> = translations.iter().map(|(_, _, z)| *z).collect();
24
25        x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
26        y.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
27        z.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
28
29        let mid = x.len() / 2;
30        let centroid = (*x.get(mid).unwrap_or(&0.0), *y.get(mid).unwrap_or(&0.0), *z.get(mid).unwrap_or(&0.0));
31
32        const THRESHOLD: f64 = 10000.0;
33        if centroid.0.abs() > THRESHOLD
34            || centroid.1.abs() > THRESHOLD
35            || centroid.2.abs() > THRESHOLD
36        {
37            return centroid;
38        }
39
40        (0.0, 0.0, 0.0)
41    }
42
43    /// Sample a building element's world-space translation for RTC offset detection.
44    /// Returns `Some((tx, ty, tz))` if the element has a valid placement transform.
45    fn sample_element_translation(
46        &self,
47        entity: &DecodedEntity,
48        decoder: &mut EntityDecoder,
49    ) -> Option<(f64, f64, f64)> {
50        let has_rep = entity.get(6).map(|a| !a.is_null()).unwrap_or(false);
51        if !has_rep {
52            return None;
53        }
54        let mut transform = self
55            .get_placement_transform_from_element(entity, decoder)
56            .ok()?;
57        self.scale_transform(&mut transform);
58        let tx = transform[(0, 3)];
59        let ty = transform[(1, 3)];
60        let tz = transform[(2, 3)];
61        if tx.is_finite() && ty.is_finite() && tz.is_finite() {
62            Some((tx, ty, tz))
63        } else {
64            None
65        }
66    }
67
68    /// Detect RTC offset by scanning the file for building elements.
69    /// Used by synchronous parse paths.
70    pub fn detect_rtc_offset_from_first_element(
71        &self,
72        content: &str,
73        decoder: &mut EntityDecoder,
74    ) -> (f64, f64, f64) {
75        use ifc_lite_core::EntityScanner;
76
77        let mut scanner = EntityScanner::new(content);
78        let mut translations: Vec<(f64, f64, f64)> = Vec::new();
79        const MAX_SAMPLES: usize = 50;
80
81        const BUILDING_ELEMENT_TYPES: &[&str] = &[
82            "IFCWALL", "IFCWALLSTANDARDCASE", "IFCSLAB", "IFCBEAM", "IFCCOLUMN",
83            "IFCPLATE", "IFCROOF", "IFCCOVERING", "IFCFOOTING", "IFCRAILING",
84            "IFCSTAIR", "IFCSTAIRFLIGHT", "IFCRAMP", "IFCRAMPFLIGHT",
85            "IFCDOOR", "IFCWINDOW", "IFCFURNISHINGELEMENT", "IFCBUILDINGELEMENTPROXY",
86            "IFCMEMBER", "IFCCURTAINWALL", "IFCPILE", "IFCSHADINGDEVICE",
87        ];
88
89        while let Some((_id, type_name, start, end)) = scanner.next_entity() {
90            if translations.len() >= MAX_SAMPLES {
91                break;
92            }
93            if !BUILDING_ELEMENT_TYPES.iter().any(|&t| t == type_name) {
94                continue;
95            }
96            if let Ok(entity) = decoder.decode_at(start, end) {
97                if let Some(t) = self.sample_element_translation(&entity, decoder) {
98                    translations.push(t);
99                }
100            }
101        }
102
103        Self::rtc_offset_from_translations(&translations)
104    }
105
106    /// Detect RTC offset using pre-collected geometry jobs (avoids re-scanning the file).
107    pub fn detect_rtc_offset_from_jobs(
108        &self,
109        jobs: &[(u32, usize, usize, IfcType)],
110        decoder: &mut EntityDecoder,
111    ) -> (f64, f64, f64) {
112        const MAX_SAMPLES: usize = 50;
113        let translations: Vec<(f64, f64, f64)> = jobs
114            .iter()
115            .take(MAX_SAMPLES)
116            .filter_map(|&(id, start, end, _)| {
117                let entity = decoder.decode_at_with_id(id, start, end).ok()?;
118                self.sample_element_translation(&entity, decoder)
119            })
120            .collect();
121
122        Self::rtc_offset_from_translations(&translations)
123    }
124
125    /// Process building element (IfcWall, IfcBeam, etc.) into mesh
126    /// Follows the representation chain:
127    /// Element → Representation → ShapeRepresentation → Items
128    #[inline]
129    pub fn process_element(
130        &self,
131        element: &DecodedEntity,
132        decoder: &mut EntityDecoder,
133    ) -> Result<Mesh> {
134        // Get representation (attribute 6 for most building elements)
135        // IfcProduct: GlobalId, OwnerHistory, Name, Description, ObjectType, ObjectPlacement, Representation, Tag
136        let representation_attr = element.get(6).ok_or_else(|| {
137            Error::geometry(format!(
138                "Element #{} has no representation attribute",
139                element.id
140            ))
141        })?;
142
143        if representation_attr.is_null() {
144            return Ok(Mesh::new()); // No geometry
145        }
146
147        let representation = decoder
148            .resolve_ref(representation_attr)?
149            .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
150
151        // IfcProductDefinitionShape has Representations attribute (list of IfcRepresentation)
152        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
153            return Err(Error::geometry(format!(
154                "Expected IfcProductDefinitionShape, got {}",
155                representation.ifc_type
156            )));
157        }
158
159        // Get representations list (attribute 2)
160        let representations_attr = representation.get(2).ok_or_else(|| {
161            Error::geometry("IfcProductDefinitionShape missing Representations".to_string())
162        })?;
163
164        let representations = decoder.resolve_ref_list(representations_attr)?;
165
166        // Process all representations and merge meshes
167        let mut combined_mesh = Mesh::new();
168
169        // First pass: check if we have any direct geometry representations
170        // This prevents duplication when both direct and MappedRepresentation exist
171        let has_direct_geometry = representations.iter().any(|rep| {
172            if rep.ifc_type != IfcType::IfcShapeRepresentation {
173                return false;
174            }
175            if let Some(rep_type_attr) = rep.get(2) {
176                if let Some(rep_type) = rep_type_attr.as_string() {
177                    matches!(
178                        rep_type,
179                        "Body"
180                            | "SweptSolid"
181                            | "SolidModel"
182                            | "Brep"
183                            | "CSG"
184                            | "Clipping"
185                            | "SurfaceModel"
186                            | "Tessellation"
187                            | "AdvancedSweptSolid"
188                            | "AdvancedBrep"
189                    )
190                } else {
191                    false
192                }
193            } else {
194                false
195            }
196        });
197
198        for shape_rep in representations {
199            if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
200                continue;
201            }
202
203            // Check RepresentationType (attribute 2) - only process geometric representations
204            // Skip 'Axis', 'Curve2D', 'FootPrint', etc. - only process 'Body', 'SweptSolid', 'Brep', etc.
205            if let Some(rep_type_attr) = shape_rep.get(2) {
206                if let Some(rep_type) = rep_type_attr.as_string() {
207                    // Skip MappedRepresentation if we already have direct geometry
208                    // This prevents duplication when an element has both direct and mapped representations
209                    if rep_type == "MappedRepresentation" && has_direct_geometry {
210                        continue;
211                    }
212
213                    // Only process solid geometry representations
214                    if !matches!(
215                        rep_type,
216                        "Body"
217                            | "SweptSolid"
218                            | "SolidModel"
219                            | "Brep"
220                            | "CSG"
221                            | "Clipping"
222                            | "SurfaceModel"
223                            | "Tessellation"
224                            | "MappedRepresentation"
225                            | "AdvancedSweptSolid"
226                            | "AdvancedBrep"
227                    ) {
228                        continue; // Skip non-solid representations like 'Axis', 'Curve2D', etc.
229                    }
230                }
231            }
232
233            // Get items list (attribute 3)
234            let items_attr = shape_rep.get(3).ok_or_else(|| {
235                Error::geometry("IfcShapeRepresentation missing Items".to_string())
236            })?;
237
238            let items = decoder.resolve_ref_list(items_attr)?;
239
240            // Process each representation item
241            for item in items {
242                let mesh = self.process_representation_item(&item, decoder)?;
243                combined_mesh.merge(&mesh);
244            }
245        }
246
247        // Apply placement transformation
248        self.apply_placement(element, decoder, &mut combined_mesh)?;
249
250        Ok(combined_mesh)
251    }
252
253    /// Process element and return sub-meshes with their geometry item IDs.
254    /// This preserves per-item identity for color/style lookup.
255    ///
256    /// For elements with multiple styled geometry items (like windows with frames + glass),
257    /// this returns separate sub-meshes that can receive different colors.
258    pub fn process_element_with_submeshes(
259        &self,
260        element: &DecodedEntity,
261        decoder: &mut EntityDecoder,
262    ) -> Result<SubMeshCollection> {
263        // Get representation (attribute 6 for most building elements)
264        let representation_attr = element.get(6).ok_or_else(|| {
265            Error::geometry(format!(
266                "Element #{} has no representation attribute",
267                element.id
268            ))
269        })?;
270
271        if representation_attr.is_null() {
272            return Ok(SubMeshCollection::new()); // No geometry
273        }
274
275        let representation = decoder
276            .resolve_ref(representation_attr)?
277            .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
278
279        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
280            return Err(Error::geometry(format!(
281                "Expected IfcProductDefinitionShape, got {}",
282                representation.ifc_type
283            )));
284        }
285
286        // Get representations list (attribute 2)
287        let representations_attr = representation.get(2).ok_or_else(|| {
288            Error::geometry("IfcProductDefinitionShape missing Representations".to_string())
289        })?;
290
291        let representations = decoder.resolve_ref_list(representations_attr)?;
292
293        let mut sub_meshes = SubMeshCollection::new();
294
295        // Check if we have direct geometry
296        let has_direct_geometry = representations.iter().any(|rep| {
297            if rep.ifc_type != IfcType::IfcShapeRepresentation {
298                return false;
299            }
300            if let Some(rep_type_attr) = rep.get(2) {
301                if let Some(rep_type) = rep_type_attr.as_string() {
302                    matches!(
303                        rep_type,
304                        "Body"
305                            | "SweptSolid"
306                            | "SolidModel"
307                            | "Brep"
308                            | "CSG"
309                            | "Clipping"
310                            | "SurfaceModel"
311                            | "Tessellation"
312                            | "AdvancedSweptSolid"
313                            | "AdvancedBrep"
314                    )
315                } else {
316                    false
317                }
318            } else {
319                false
320            }
321        });
322
323        for shape_rep in representations {
324            if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
325                continue;
326            }
327
328            if let Some(rep_type_attr) = shape_rep.get(2) {
329                if let Some(rep_type) = rep_type_attr.as_string() {
330                    // Skip MappedRepresentation if we have direct geometry
331                    if rep_type == "MappedRepresentation" && has_direct_geometry {
332                        continue;
333                    }
334
335                    // Only process solid geometry representations
336                    if !matches!(
337                        rep_type,
338                        "Body"
339                            | "SweptSolid"
340                            | "SolidModel"
341                            | "Brep"
342                            | "CSG"
343                            | "Clipping"
344                            | "SurfaceModel"
345                            | "Tessellation"
346                            | "MappedRepresentation"
347                            | "AdvancedSweptSolid"
348                            | "AdvancedBrep"
349                    ) {
350                        continue;
351                    }
352                }
353            }
354
355            // Get items list (attribute 3)
356            let items_attr = shape_rep.get(3).ok_or_else(|| {
357                Error::geometry("IfcShapeRepresentation missing Items".to_string())
358            })?;
359
360            let items = decoder.resolve_ref_list(items_attr)?;
361
362            // Process each representation item, preserving geometry IDs
363            for item in items {
364                self.collect_submeshes_from_item(&item, decoder, &mut sub_meshes)?;
365            }
366        }
367
368        // Apply placement transformation to all sub-meshes
369        // ObjectPlacement translation is in file units (e.g., mm) but geometry is scaled to meters,
370        // so we MUST scale the transform to match. Same as apply_placement does.
371        if let Some(placement_attr) = element.get(5) {
372            if !placement_attr.is_null() {
373                if let Some(placement) = decoder.resolve_ref(placement_attr)? {
374                    let mut transform = self.get_placement_transform(&placement, decoder)?;
375                    self.scale_transform(&mut transform);
376                    for sub in &mut sub_meshes.sub_meshes {
377                        self.transform_mesh(&mut sub.mesh, &transform);
378                    }
379                }
380            }
381        }
382
383        Ok(sub_meshes)
384    }
385
386    /// Collect sub-meshes from a representation item, following MappedItem references.
387    fn collect_submeshes_from_item(
388        &self,
389        item: &DecodedEntity,
390        decoder: &mut EntityDecoder,
391        sub_meshes: &mut SubMeshCollection,
392    ) -> Result<()> {
393        // For MappedItem, recurse into the mapped representation
394        if item.ifc_type == IfcType::IfcMappedItem {
395            // Get MappingSource (RepresentationMap)
396            let source_attr = item
397                .get(0)
398                .ok_or_else(|| Error::geometry("MappedItem missing MappingSource".to_string()))?;
399
400            let source_entity = decoder
401                .resolve_ref(source_attr)?
402                .ok_or_else(|| Error::geometry("Failed to resolve MappingSource".to_string()))?;
403
404            // Get MappedRepresentation from RepresentationMap (attribute 1)
405            let mapped_repr_attr = source_entity
406                .get(1)
407                .ok_or_else(|| Error::geometry("RepresentationMap missing MappedRepresentation".to_string()))?;
408
409            let mapped_repr = decoder
410                .resolve_ref(mapped_repr_attr)?
411                .ok_or_else(|| Error::geometry("Failed to resolve MappedRepresentation".to_string()))?;
412
413            // Get MappingTarget transformation
414            let mapping_transform = if let Some(target_attr) = item.get(1) {
415                if !target_attr.is_null() {
416                    if let Some(target_entity) = decoder.resolve_ref(target_attr)? {
417                        Some(self.parse_cartesian_transformation_operator(&target_entity, decoder)?)
418                    } else {
419                        None
420                    }
421                } else {
422                    None
423                }
424            } else {
425                None
426            };
427
428            // Get items from the mapped representation
429            if let Some(items_attr) = mapped_repr.get(3) {
430                let items = decoder.resolve_ref_list(items_attr)?;
431                for nested_item in items {
432                    // Recursively collect sub-meshes (skip unsupported geometry types)
433                    let count_before = sub_meshes.len();
434                    if let Err(_e) = self
435                        .collect_submeshes_from_item(&nested_item, decoder, sub_meshes)
436                    {
437                        #[cfg(debug_assertions)]
438                        eprintln!(
439                            "[ifc-lite] Skipping unsupported nested geometry #{} ({:?}): {}",
440                            nested_item.id, nested_item.ifc_type, _e
441                        );
442                        continue;
443                    }
444
445                    // Apply MappedItem transform to newly added sub-meshes
446                    if let Some(mut transform) = mapping_transform.clone() {
447                        self.scale_transform(&mut transform);
448                        for sub in &mut sub_meshes.sub_meshes[count_before..] {
449                            self.transform_mesh(&mut sub.mesh, &transform);
450                        }
451                    }
452                }
453            }
454        } else {
455            // Regular geometry item - process and record with its ID
456            // Skip unsupported geometry types (e.g. IfcGeometricSet) instead of failing
457            match self.process_representation_item(item, decoder) {
458                Ok(mesh) => {
459                    if !mesh.is_empty() {
460                        sub_meshes.add(item.id, mesh);
461                    }
462                }
463                Err(_e) => {
464                    #[cfg(debug_assertions)]
465                    eprintln!(
466                        "[ifc-lite] Skipping unsupported geometry #{} ({:?}): {}",
467                        item.id, item.ifc_type, _e
468                    );
469                }
470            }
471        }
472
473        Ok(())
474    }
475
476    /// Process building element and return geometry + transform separately
477    /// Used for instanced rendering - geometry is returned untransformed, transform is separate
478    #[inline]
479    pub fn process_element_with_transform(
480        &self,
481        element: &DecodedEntity,
482        decoder: &mut EntityDecoder,
483    ) -> Result<(Mesh, Matrix4<f64>)> {
484        // Get representation (attribute 6 for most building elements)
485        let representation_attr = element.get(6).ok_or_else(|| {
486            Error::geometry(format!(
487                "Element #{} has no representation attribute",
488                element.id
489            ))
490        })?;
491
492        if representation_attr.is_null() {
493            return Ok((Mesh::new(), Matrix4::identity())); // No geometry
494        }
495
496        let representation = decoder
497            .resolve_ref(representation_attr)?
498            .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
499
500        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
501            return Err(Error::geometry(format!(
502                "Expected IfcProductDefinitionShape, got {}",
503                representation.ifc_type
504            )));
505        }
506
507        // Get representations list (attribute 2)
508        let representations_attr = representation.get(2).ok_or_else(|| {
509            Error::geometry("IfcProductDefinitionShape missing Representations".to_string())
510        })?;
511
512        let representations = decoder.resolve_ref_list(representations_attr)?;
513
514        // Process all representations and merge meshes
515        let mut combined_mesh = Mesh::new();
516
517        // Check for direct geometry
518        let has_direct_geometry = representations.iter().any(|rep| {
519            if rep.ifc_type != IfcType::IfcShapeRepresentation {
520                return false;
521            }
522            if let Some(rep_type_attr) = rep.get(2) {
523                if let Some(rep_type) = rep_type_attr.as_string() {
524                    matches!(
525                        rep_type,
526                        "Body"
527                            | "SweptSolid"
528                            | "SolidModel"
529                            | "Brep"
530                            | "CSG"
531                            | "Clipping"
532                            | "SurfaceModel"
533                            | "Tessellation"
534                            | "AdvancedSweptSolid"
535                            | "AdvancedBrep"
536                    )
537                } else {
538                    false
539                }
540            } else {
541                false
542            }
543        });
544
545        for shape_rep in representations {
546            if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
547                continue;
548            }
549
550            if let Some(rep_type_attr) = shape_rep.get(2) {
551                if let Some(rep_type) = rep_type_attr.as_string() {
552                    if rep_type == "MappedRepresentation" && has_direct_geometry {
553                        continue;
554                    }
555
556                    if !matches!(
557                        rep_type,
558                        "Body"
559                            | "SweptSolid"
560                            | "SolidModel"
561                            | "Brep"
562                            | "CSG"
563                            | "Clipping"
564                            | "SurfaceModel"
565                            | "Tessellation"
566                            | "MappedRepresentation"
567                            | "AdvancedSweptSolid"
568                            | "AdvancedBrep"
569                    ) {
570                        continue;
571                    }
572                }
573            }
574
575            let items_attr = shape_rep.get(3).ok_or_else(|| {
576                Error::geometry("IfcShapeRepresentation missing Items".to_string())
577            })?;
578
579            let items = decoder.resolve_ref_list(items_attr)?;
580
581            for item in items {
582                let mesh = self.process_representation_item(&item, decoder)?;
583                combined_mesh.merge(&mesh);
584            }
585        }
586
587        // Get placement transform WITHOUT applying it
588        let transform = self.get_placement_transform_from_element(element, decoder)?;
589
590        Ok((combined_mesh, transform))
591    }
592
593    /// Process a single representation item (IfcExtrudedAreaSolid, etc.)
594    /// Uses hash-based caching for geometry deduplication across repeated floors
595    #[inline]
596    pub fn process_representation_item(
597        &self,
598        item: &DecodedEntity,
599        decoder: &mut EntityDecoder,
600    ) -> Result<Mesh> {
601        // Special handling for MappedItem with caching
602        if item.ifc_type == IfcType::IfcMappedItem {
603            return self.process_mapped_item_cached(item, decoder);
604        }
605
606        // Check FacetedBrep cache first (from batch preprocessing)
607        if item.ifc_type == IfcType::IfcFacetedBrep {
608            if let Some(mut mesh) = self.take_cached_faceted_brep(item.id) {
609                self.scale_mesh(&mut mesh);
610                let cached = self.get_or_cache_by_hash(mesh);
611                return Ok((*cached).clone());
612            }
613        }
614
615        // Check if we have a processor for this type
616        if let Some(processor) = self.processors.get(&item.ifc_type) {
617            let mut mesh = processor.process(item, decoder, &self.schema)?;
618            self.scale_mesh(&mut mesh);
619
620            // Deduplicate by hash - buildings with repeated floors have identical geometry
621            if !mesh.positions.is_empty() {
622                let cached = self.get_or_cache_by_hash(mesh);
623                return Ok((*cached).clone());
624            }
625            return Ok(mesh);
626        }
627
628        // Check category for fallback handling
629        match self.schema.geometry_category(&item.ifc_type) {
630            Some(GeometryCategory::SweptSolid) => {
631                // For now, return empty mesh - processors will handle this
632                Ok(Mesh::new())
633            }
634            Some(GeometryCategory::ExplicitMesh) => {
635                // For now, return empty mesh - processors will handle this
636                Ok(Mesh::new())
637            }
638            Some(GeometryCategory::Boolean) => {
639                // For now, return empty mesh - processors will handle this
640                Ok(Mesh::new())
641            }
642            Some(GeometryCategory::MappedItem) => {
643                // For now, return empty mesh - processors will handle this
644                Ok(Mesh::new())
645            }
646            _ => Err(Error::geometry(format!(
647                "Unsupported representation type: {}",
648                item.ifc_type
649            ))),
650        }
651    }
652
653    /// Process MappedItem with caching for repeated geometry
654    #[inline]
655    fn process_mapped_item_cached(
656        &self,
657        item: &DecodedEntity,
658        decoder: &mut EntityDecoder,
659    ) -> Result<Mesh> {
660        // IfcMappedItem attributes:
661        // 0: MappingSource (IfcRepresentationMap)
662        // 1: MappingTarget (IfcCartesianTransformationOperator)
663
664        // Get mapping source (RepresentationMap)
665        let source_attr = item
666            .get(0)
667            .ok_or_else(|| Error::geometry("MappedItem missing MappingSource".to_string()))?;
668
669        let source_entity = decoder
670            .resolve_ref(source_attr)?
671            .ok_or_else(|| Error::geometry("Failed to resolve MappingSource".to_string()))?;
672
673        let source_id = source_entity.id;
674
675        // Get MappingTarget transformation (attribute 1: CartesianTransformationOperator)
676        let mapping_transform = if let Some(target_attr) = item.get(1) {
677            if !target_attr.is_null() {
678                if let Some(target_entity) = decoder.resolve_ref(target_attr)? {
679                    Some(self.parse_cartesian_transformation_operator(&target_entity, decoder)?)
680                } else {
681                    None
682                }
683            } else {
684                None
685            }
686        } else {
687            None
688        };
689
690        // Check cache first
691        {
692            let cache = self.mapped_item_cache.borrow();
693            if let Some(cached_mesh) = cache.get(&source_id) {
694                let mut mesh = cached_mesh.as_ref().clone();
695                if let Some(mut transform) = mapping_transform {
696                    self.scale_transform(&mut transform);
697                    self.transform_mesh(&mut mesh, &transform);
698                }
699                return Ok(mesh);
700            }
701        }
702
703        // Cache miss - process the geometry
704        // IfcRepresentationMap has:
705        // 0: MappingOrigin (IfcAxis2Placement)
706        // 1: MappedRepresentation (IfcRepresentation)
707
708        let mapped_rep_attr = source_entity.get(1).ok_or_else(|| {
709            Error::geometry("RepresentationMap missing MappedRepresentation".to_string())
710        })?;
711
712        let mapped_rep = decoder
713            .resolve_ref(mapped_rep_attr)?
714            .ok_or_else(|| Error::geometry("Failed to resolve MappedRepresentation".to_string()))?;
715
716        // Get representation items
717        let items_attr = mapped_rep
718            .get(3)
719            .ok_or_else(|| Error::geometry("Representation missing Items".to_string()))?;
720
721        let items = decoder.resolve_ref_list(items_attr)?;
722
723        // Process all items and merge (without recursing into MappedItem to avoid infinite loop)
724        let mut mesh = Mesh::new();
725        for sub_item in items {
726            if sub_item.ifc_type == IfcType::IfcMappedItem {
727                continue; // Skip nested MappedItems to avoid recursion
728            }
729            if let Some(processor) = self.processors.get(&sub_item.ifc_type) {
730                if let Ok(mut sub_mesh) = processor.process(&sub_item, decoder, &self.schema) {
731                    self.scale_mesh(&mut sub_mesh);
732                    mesh.merge(&sub_mesh);
733                }
734            }
735        }
736
737        // Store in cache (before transformation, so cached mesh is in source coordinates)
738        {
739            let mut cache = self.mapped_item_cache.borrow_mut();
740            cache.insert(source_id, Arc::new(mesh.clone()));
741        }
742
743        // Apply MappingTarget transformation to this instance
744        if let Some(mut transform) = mapping_transform {
745            self.scale_transform(&mut transform);
746            self.transform_mesh(&mut mesh, &transform);
747        }
748
749        Ok(mesh)
750    }
751}