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