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
433                    let count_before = sub_meshes.len();
434                    self.collect_submeshes_from_item(&nested_item, decoder, sub_meshes)?;
435
436                    // Apply MappedItem transform to newly added sub-meshes
437                    if let Some(mut transform) = mapping_transform.clone() {
438                        self.scale_transform(&mut transform);
439                        for sub in &mut sub_meshes.sub_meshes[count_before..] {
440                            self.transform_mesh(&mut sub.mesh, &transform);
441                        }
442                    }
443                }
444            }
445        } else {
446            // Regular geometry item - process and record with its ID
447            let mesh = self.process_representation_item(item, decoder)?;
448            if !mesh.is_empty() {
449                sub_meshes.add(item.id, mesh);
450            }
451        }
452
453        Ok(())
454    }
455
456    /// Process building element and return geometry + transform separately
457    /// Used for instanced rendering - geometry is returned untransformed, transform is separate
458    #[inline]
459    pub fn process_element_with_transform(
460        &self,
461        element: &DecodedEntity,
462        decoder: &mut EntityDecoder,
463    ) -> Result<(Mesh, Matrix4<f64>)> {
464        // Get representation (attribute 6 for most building elements)
465        let representation_attr = element.get(6).ok_or_else(|| {
466            Error::geometry(format!(
467                "Element #{} has no representation attribute",
468                element.id
469            ))
470        })?;
471
472        if representation_attr.is_null() {
473            return Ok((Mesh::new(), Matrix4::identity())); // No geometry
474        }
475
476        let representation = decoder
477            .resolve_ref(representation_attr)?
478            .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
479
480        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
481            return Err(Error::geometry(format!(
482                "Expected IfcProductDefinitionShape, got {}",
483                representation.ifc_type
484            )));
485        }
486
487        // Get representations list (attribute 2)
488        let representations_attr = representation.get(2).ok_or_else(|| {
489            Error::geometry("IfcProductDefinitionShape missing Representations".to_string())
490        })?;
491
492        let representations = decoder.resolve_ref_list(representations_attr)?;
493
494        // Process all representations and merge meshes
495        let mut combined_mesh = Mesh::new();
496
497        // Check for direct geometry
498        let has_direct_geometry = representations.iter().any(|rep| {
499            if rep.ifc_type != IfcType::IfcShapeRepresentation {
500                return false;
501            }
502            if let Some(rep_type_attr) = rep.get(2) {
503                if let Some(rep_type) = rep_type_attr.as_string() {
504                    matches!(
505                        rep_type,
506                        "Body"
507                            | "SweptSolid"
508                            | "SolidModel"
509                            | "Brep"
510                            | "CSG"
511                            | "Clipping"
512                            | "SurfaceModel"
513                            | "Tessellation"
514                            | "AdvancedSweptSolid"
515                            | "AdvancedBrep"
516                    )
517                } else {
518                    false
519                }
520            } else {
521                false
522            }
523        });
524
525        for shape_rep in representations {
526            if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
527                continue;
528            }
529
530            if let Some(rep_type_attr) = shape_rep.get(2) {
531                if let Some(rep_type) = rep_type_attr.as_string() {
532                    if rep_type == "MappedRepresentation" && has_direct_geometry {
533                        continue;
534                    }
535
536                    if !matches!(
537                        rep_type,
538                        "Body"
539                            | "SweptSolid"
540                            | "SolidModel"
541                            | "Brep"
542                            | "CSG"
543                            | "Clipping"
544                            | "SurfaceModel"
545                            | "Tessellation"
546                            | "MappedRepresentation"
547                            | "AdvancedSweptSolid"
548                            | "AdvancedBrep"
549                    ) {
550                        continue;
551                    }
552                }
553            }
554
555            let items_attr = shape_rep.get(3).ok_or_else(|| {
556                Error::geometry("IfcShapeRepresentation missing Items".to_string())
557            })?;
558
559            let items = decoder.resolve_ref_list(items_attr)?;
560
561            for item in items {
562                let mesh = self.process_representation_item(&item, decoder)?;
563                combined_mesh.merge(&mesh);
564            }
565        }
566
567        // Get placement transform WITHOUT applying it
568        let transform = self.get_placement_transform_from_element(element, decoder)?;
569
570        Ok((combined_mesh, transform))
571    }
572
573    /// Process a single representation item (IfcExtrudedAreaSolid, etc.)
574    /// Uses hash-based caching for geometry deduplication across repeated floors
575    #[inline]
576    pub fn process_representation_item(
577        &self,
578        item: &DecodedEntity,
579        decoder: &mut EntityDecoder,
580    ) -> Result<Mesh> {
581        // Special handling for MappedItem with caching
582        if item.ifc_type == IfcType::IfcMappedItem {
583            return self.process_mapped_item_cached(item, decoder);
584        }
585
586        // Check FacetedBrep cache first (from batch preprocessing)
587        if item.ifc_type == IfcType::IfcFacetedBrep {
588            if let Some(mut mesh) = self.take_cached_faceted_brep(item.id) {
589                self.scale_mesh(&mut mesh);
590                let cached = self.get_or_cache_by_hash(mesh);
591                return Ok((*cached).clone());
592            }
593        }
594
595        // Check if we have a processor for this type
596        if let Some(processor) = self.processors.get(&item.ifc_type) {
597            let mut mesh = processor.process(item, decoder, &self.schema)?;
598            self.scale_mesh(&mut mesh);
599
600            // Deduplicate by hash - buildings with repeated floors have identical geometry
601            if !mesh.positions.is_empty() {
602                let cached = self.get_or_cache_by_hash(mesh);
603                return Ok((*cached).clone());
604            }
605            return Ok(mesh);
606        }
607
608        // Check category for fallback handling
609        match self.schema.geometry_category(&item.ifc_type) {
610            Some(GeometryCategory::SweptSolid) => {
611                // For now, return empty mesh - processors will handle this
612                Ok(Mesh::new())
613            }
614            Some(GeometryCategory::ExplicitMesh) => {
615                // For now, return empty mesh - processors will handle this
616                Ok(Mesh::new())
617            }
618            Some(GeometryCategory::Boolean) => {
619                // For now, return empty mesh - processors will handle this
620                Ok(Mesh::new())
621            }
622            Some(GeometryCategory::MappedItem) => {
623                // For now, return empty mesh - processors will handle this
624                Ok(Mesh::new())
625            }
626            _ => Err(Error::geometry(format!(
627                "Unsupported representation type: {}",
628                item.ifc_type
629            ))),
630        }
631    }
632
633    /// Process MappedItem with caching for repeated geometry
634    #[inline]
635    fn process_mapped_item_cached(
636        &self,
637        item: &DecodedEntity,
638        decoder: &mut EntityDecoder,
639    ) -> Result<Mesh> {
640        // IfcMappedItem attributes:
641        // 0: MappingSource (IfcRepresentationMap)
642        // 1: MappingTarget (IfcCartesianTransformationOperator)
643
644        // Get mapping source (RepresentationMap)
645        let source_attr = item
646            .get(0)
647            .ok_or_else(|| Error::geometry("MappedItem missing MappingSource".to_string()))?;
648
649        let source_entity = decoder
650            .resolve_ref(source_attr)?
651            .ok_or_else(|| Error::geometry("Failed to resolve MappingSource".to_string()))?;
652
653        let source_id = source_entity.id;
654
655        // Get MappingTarget transformation (attribute 1: CartesianTransformationOperator)
656        let mapping_transform = if let Some(target_attr) = item.get(1) {
657            if !target_attr.is_null() {
658                if let Some(target_entity) = decoder.resolve_ref(target_attr)? {
659                    Some(self.parse_cartesian_transformation_operator(&target_entity, decoder)?)
660                } else {
661                    None
662                }
663            } else {
664                None
665            }
666        } else {
667            None
668        };
669
670        // Check cache first
671        {
672            let cache = self.mapped_item_cache.borrow();
673            if let Some(cached_mesh) = cache.get(&source_id) {
674                let mut mesh = cached_mesh.as_ref().clone();
675                if let Some(mut transform) = mapping_transform {
676                    self.scale_transform(&mut transform);
677                    self.transform_mesh(&mut mesh, &transform);
678                }
679                return Ok(mesh);
680            }
681        }
682
683        // Cache miss - process the geometry
684        // IfcRepresentationMap has:
685        // 0: MappingOrigin (IfcAxis2Placement)
686        // 1: MappedRepresentation (IfcRepresentation)
687
688        let mapped_rep_attr = source_entity.get(1).ok_or_else(|| {
689            Error::geometry("RepresentationMap missing MappedRepresentation".to_string())
690        })?;
691
692        let mapped_rep = decoder
693            .resolve_ref(mapped_rep_attr)?
694            .ok_or_else(|| Error::geometry("Failed to resolve MappedRepresentation".to_string()))?;
695
696        // Get representation items
697        let items_attr = mapped_rep
698            .get(3)
699            .ok_or_else(|| Error::geometry("Representation missing Items".to_string()))?;
700
701        let items = decoder.resolve_ref_list(items_attr)?;
702
703        // Process all items and merge (without recursing into MappedItem to avoid infinite loop)
704        let mut mesh = Mesh::new();
705        for sub_item in items {
706            if sub_item.ifc_type == IfcType::IfcMappedItem {
707                continue; // Skip nested MappedItems to avoid recursion
708            }
709            if let Some(processor) = self.processors.get(&sub_item.ifc_type) {
710                if let Ok(mut sub_mesh) = processor.process(&sub_item, decoder, &self.schema) {
711                    self.scale_mesh(&mut sub_mesh);
712                    mesh.merge(&sub_mesh);
713                }
714            }
715        }
716
717        // Store in cache (before transformation, so cached mesh is in source coordinates)
718        {
719            let mut cache = self.mapped_item_cache.borrow_mut();
720            cache.insert(source_id, Arc::new(mesh.clone()));
721        }
722
723        // Apply MappingTarget transformation to this instance
724        if let Some(mut transform) = mapping_transform {
725            self.scale_transform(&mut transform);
726            self.transform_mesh(&mut mesh, &transform);
727        }
728
729        Ok(mesh)
730    }
731}