ifc_lite_geometry/router/
textured.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//! The router's surface-texture channel (#961, #1781): texture-aware
6//! representation-map tessellation (orphan type geometry) and the textured
7//! occurrence sub-mesh entry point. Split from `processing.rs` so the main
8//! element pipeline stays within the module-size house rule; the texture
9//! index itself is built in `crate::processors::texture`.
10
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
12
13use super::frame_parts::{single_frame_source, SourceParts};
14use super::GeometryRouter;
15use crate::{Error, Mesh, Result, SubMeshCollection};
16
17impl GeometryRouter {
18    /// Texture-aware [`Self::process_element_with_submeshes`] (#1781): a face
19    /// set listed in `texture_index` becomes its own textured sub-mesh carrying
20    /// per-vertex UVs + the texture attachment. The void path never passes an
21    /// index — a CSG cut rebuilds vertices, which would orphan the UVs, so a
22    /// voided textured element renders with its style colour instead.
23    pub fn process_element_with_submeshes_textured(
24        &self,
25        element: &DecodedEntity,
26        decoder: &mut EntityDecoder,
27        texture_index: &rustc_hash::FxHashMap<u32, crate::processors::texture::ResolvedTextureMap>,
28    ) -> Result<SubMeshCollection> {
29        let textures = if texture_index.is_empty() {
30            None
31        } else {
32            Some(texture_index)
33        };
34        self.process_element_with_submeshes_impl(
35            element,
36            decoder,
37            true,
38            textures,
39            super::processing::SourceHygiene::Watertight,
40        )
41    }
42
43    /// Tessellate an `IfcRepresentationMap`'s `MappedRepresentation` and bake
44    /// its `MappingOrigin` placement (issue #957).
45    ///
46    /// Used to render geometry that hangs off an `IfcTypeProduct` (e.g.
47    /// `IfcBoilerType`) through its `RepresentationMaps` when no occurrence
48    /// instantiates it — the buildingSMART annex-E "tessellated shape with
49    /// style" samples ship exactly this shape (geometry on the type, declared
50    /// via `IfcRelDeclares`, with no product instance).
51    ///
52    /// Unlike [`Self::process_mapped_item_cached`], this applies `MappingOrigin`
53    /// (`IfcRepresentationMap` attr 0) rather than a `MappingTarget`: there is
54    /// no occurrence placement and no `IfcMappedItem` to carry one, so the
55    /// MappingOrigin axis placement is the only transform. It is the caller's
56    /// responsibility to only invoke this for orphan representation maps so
57    /// normally-instanced typed products aren't double-rendered.
58    ///
59    /// Errors when the map's items cannot share one f64 frame (#6446); use
60    /// [`Self::process_representation_map_with_texture`] to keep such a map
61    /// at full precision.
62    pub fn process_representation_map(
63        &self,
64        rep_map: &DecodedEntity,
65        decoder: &mut EntityDecoder,
66    ) -> Result<Mesh> {
67        let empty = rustc_hash::FxHashMap::default();
68        let parts = self.process_representation_map_with_texture(rep_map, decoder, &empty)?;
69        let mut mesh = SourceParts::default();
70        for (part, _uvs, _texture) in parts {
71            mesh.merge(&part);
72        }
73        single_frame_source("IfcRepresentationMap", rep_map.id, mesh.into_parts())
74    }
75
76    /// Texture-aware variant of [`Self::process_representation_map`] (issue
77    /// #961). Returns one render part per output mesh: each textured
78    /// `IfcTriangulatedFaceSet` item becomes its OWN part carrying its UVs +
79    /// decoded image (so a representation with several differently-textured
80    /// items renders each with the correct image), and all untextured items are
81    /// merged into a single part with empty UVs / no texture, except that
82    /// untextured items in frames at least 1 km apart stay separate untextured
83    /// parts (#6446). The MappingOrigin placement is baked into every part.
84    pub fn process_representation_map_with_texture(
85        &self,
86        rep_map: &DecodedEntity,
87        decoder: &mut EntityDecoder,
88        texture_index: &rustc_hash::FxHashMap<u32, crate::processors::texture::ResolvedTextureMap>,
89    ) -> Result<
90        Vec<(
91            Mesh,
92            Vec<f32>,
93            Option<crate::processors::texture::TextureAttachment>,
94        )>,
95    > {
96        // attr 1: MappedRepresentation (IfcShapeRepresentation)
97        let mapped_rep_attr = rep_map.get(1).ok_or_else(|| {
98            Error::geometry("RepresentationMap missing MappedRepresentation".to_string())
99        })?;
100        let mapped_rep = decoder
101            .resolve_ref(mapped_rep_attr)?
102            .ok_or_else(|| Error::geometry("Failed to resolve MappedRepresentation".to_string()))?;
103
104        // attr 3: Items
105        let items_attr = mapped_rep
106            .get(3)
107            .ok_or_else(|| Error::geometry("Representation missing Items".to_string()))?;
108        let items = decoder.resolve_ref_list(items_attr)?;
109
110        // Whether a dropped item here is a real content gap, or just this router
111        // declining to mesh a 2D representation it was never meant to mesh.
112        //
113        // The occurrence path filters representations with `is_body_representation`
114        // before it ever reaches an item (`processing.rs`, "Skip 'Axis', 'Curve2D',
115        // 'FootPrint'"). The TYPE path does not: `plan_type_geometry` selects
116        // RepresentationMaps by reference/instantiation only and never looks at the
117        // identifier, so a Revit/ArchiCAD `IfcDoorType` carrying a 'FootPrint' or
118        // 'Annotation' map hands us `IfcGeometricCurveSet` / `IfcPolyline` /
119        // `IfcAnnotationFillArea` — none of which have a processor, all of which are
120        // CORRECTLY absent from a 3D view. Counting those made a clean model warn
121        // "N representation items dropped ... elements are missing or incomplete",
122        // which is exactly the false positive that would teach users to ignore it.
123        //
124        // Only the counting is gated, not the walk: a non-body map still runs
125        // through the loop (merging nothing) so geometry output is byte-identical.
126        // The scope also makes the count per SOURCE rather than per call — this
127        // map may already have been walked through `mapped_item.rs` — see
128        // `GeometryRouter::enter_unsupported_source`.
129        let _drop_scope = self.enter_unsupported_source(rep_map.id, &mapped_rep);
130
131        let mut untextured = SourceParts::default();
132        // One entry per textured item — keeps each item with its own image.
133        let mut textured: Vec<(
134            Mesh,
135            Vec<f32>,
136            crate::processors::texture::TextureAttachment,
137        )> = Vec::new();
138        for item in items {
139            // A nested IfcMappedItem inside a type's own representation: process
140            // it (applies its MappingTarget) rather than dropping its geometry.
141            if item.ifc_type == IfcType::IfcMappedItem {
142                match self.process_mapped_item_parts(&item, decoder) {
143                    // already scaled inside the cached path
144                    Ok(sub_parts) => sub_parts.iter().for_each(|part| untextured.merge(part)),
145                    Err(_e) => {
146                        self.record_unsupported_item(item.ifc_type.clone());
147                        crate::diag::diag_debug!(
148                            { item_id = item.id, error = %_e,
149                              "skipping unsupported nested IfcMappedItem in representation map" }
150                            else {
151                                #[cfg(debug_assertions)]
152                                eprintln!(
153                                    "[ifc-lite] Skipping unsupported nested IfcMappedItem #{} in representation map: {}",
154                                    item.id, _e
155                                );
156                            }
157                        );
158                    }
159                }
160                continue;
161            }
162
163            // Textured tessellated face set → its own part with per-vertex UVs (#961);
164            // a terrain TIN is a face set that only appends `Flags` (#5942).
165            if matches!(item.ifc_type, IfcType::IfcTriangulatedFaceSet | IfcType::IfcTriangulatedIrregularNetwork) {
166                if let Some(map) = texture_index.get(&item.id) {
167                    let proc = crate::processors::TriangulatedFaceSetProcessor::new();
168                    if let Ok((mut sub_mesh, sub_uvs)) =
169                        proc.process_with_texture(&item, decoder, map)
170                    {
171                        self.scale_mesh(&mut sub_mesh); // UVs are unaffected by scale
172                        textured.push((sub_mesh, sub_uvs, map.attachment()));
173                        continue;
174                    }
175                }
176            }
177
178            match self.processors.get(&item.ifc_type, self.schema) {
179                Some(processor) => match processor.process(
180                    &item,
181                    decoder,
182                    self.schema,
183                    self.tessellation_quality,
184                ) {
185                    Ok(mut sub_mesh) => {
186                        sub_mesh.validate_indices();
187                        self.scale_mesh(&mut sub_mesh);
188                        untextured.merge(&sub_mesh);
189                    }
190                    Err(_e) => {
191                        self.record_unsupported_item(item.ifc_type.clone());
192                        crate::diag::diag_debug!(
193                            { item_id = item.id, ifc_type = ?item.ifc_type,
194                              error = %_e, "skipping unsupported representation-map item" }
195                            else {
196                                #[cfg(debug_assertions)]
197                                eprintln!(
198                                    "[ifc-lite] Skipping unsupported representation-map item #{} ({:?}): {}",
199                                    item.id, item.ifc_type, _e
200                                );
201                            }
202                        );
203                    }
204                },
205                None => {
206                    self.record_unsupported_item(item.ifc_type.clone());
207                    crate::diag::diag_debug!(
208                        { item_id = item.id, ifc_type = ?item.ifc_type,
209                          "skipping unsupported representation-map item (no processor)" }
210                        else {
211                            #[cfg(debug_assertions)]
212                            eprintln!(
213                                "[ifc-lite] Skipping unsupported representation-map item #{} ({:?}): no processor",
214                                item.id, item.ifc_type
215                            );
216                        }
217                    );
218                }
219            }
220        }
221
222        // attr 0: MappingOrigin (IfcAxis2Placement3D) — the only 3D transform;
223        // UVs are 2D and unaffected. Parse once, bake into every part.
224        let origin_transform: Option<nalgebra::Matrix4<f64>> = match rep_map.get(0) {
225            Some(origin_attr) if !origin_attr.is_null() => {
226                match decoder.resolve_ref(origin_attr)? {
227                Some(origin) if origin.ifc_type == IfcType::IfcAxis2Placement3D => {
228                    let mut t = self.parse_axis2_placement_3d(&origin, decoder)?;
229                    self.scale_transform(&mut t);
230                    Some(t)
231                }
232                _ => None,
233                }
234            }
235            _ => None,
236        };
237
238        let mut out: Vec<(
239            Mesh,
240            Vec<f32>,
241            Option<crate::processors::texture::TextureAttachment>,
242        )> = Vec::new();
243        for (mut mesh, uvs, texture) in textured {
244            if let Some(t) = &origin_transform {
245                self.transform_mesh_local(&mut mesh, t);
246            }
247            // Same sliver hygiene as the other mesh-output chokepoints. This is
248            // the type-geometry (RepresentationMap) channel and the only one
249            // carrying a parallel per-vertex UV array; clean_degenerate edits
250            // only indices (vertices/UVs untouched), so the UVs stay in sync.
251            mesh.clean_degenerate();
252            out.push((mesh, uvs, Some(texture)));
253        }
254        for mut untextured in untextured.into_parts().into_iter().filter(|mesh| !mesh.is_empty()) {
255            if let Some(t) = &origin_transform {
256                self.transform_mesh_local(&mut untextured, t);
257            }
258            untextured.clean_degenerate();
259            out.push((untextured, Vec::new(), None));
260        }
261
262        Ok(out)
263    }
264}