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::GeometryRouter;
14use crate::{Error, Mesh, Result, SubMeshCollection};
15
16impl GeometryRouter {
17 /// Texture-aware [`Self::process_element_with_submeshes`] (#1781): a face
18 /// set listed in `texture_index` becomes its own textured sub-mesh carrying
19 /// per-vertex UVs + the texture attachment. The void path never passes an
20 /// index — a CSG cut rebuilds vertices, which would orphan the UVs, so a
21 /// voided textured element renders with its style colour instead.
22 pub fn process_element_with_submeshes_textured(
23 &self,
24 element: &DecodedEntity,
25 decoder: &mut EntityDecoder,
26 texture_index: &rustc_hash::FxHashMap<u32, crate::processors::texture::ResolvedTextureMap>,
27 ) -> Result<SubMeshCollection> {
28 let textures = if texture_index.is_empty() {
29 None
30 } else {
31 Some(texture_index)
32 };
33 self.process_element_with_submeshes_impl(element, decoder, true, textures)
34 }
35
36 /// Tessellate an `IfcRepresentationMap`'s `MappedRepresentation` and bake
37 /// its `MappingOrigin` placement (issue #957).
38 ///
39 /// Used to render geometry that hangs off an `IfcTypeProduct` (e.g.
40 /// `IfcBoilerType`) through its `RepresentationMaps` when no occurrence
41 /// instantiates it — the buildingSMART annex-E "tessellated shape with
42 /// style" samples ship exactly this shape (geometry on the type, declared
43 /// via `IfcRelDeclares`, with no product instance).
44 ///
45 /// Unlike [`Self::process_mapped_item_cached`], this applies `MappingOrigin`
46 /// (`IfcRepresentationMap` attr 0) rather than a `MappingTarget`: there is
47 /// no occurrence placement and no `IfcMappedItem` to carry one, so the
48 /// MappingOrigin axis placement is the only transform. It is the caller's
49 /// responsibility to only invoke this for orphan representation maps so
50 /// normally-instanced typed products aren't double-rendered.
51 pub fn process_representation_map(
52 &self,
53 rep_map: &DecodedEntity,
54 decoder: &mut EntityDecoder,
55 ) -> Result<Mesh> {
56 let empty = rustc_hash::FxHashMap::default();
57 let parts = self.process_representation_map_with_texture(rep_map, decoder, &empty)?;
58 let mut mesh = Mesh::new();
59 for (part, _uvs, _texture) in parts {
60 mesh.merge(&part);
61 }
62 Ok(mesh)
63 }
64
65 /// Texture-aware variant of [`Self::process_representation_map`] (issue
66 /// #961). Returns one render part per output mesh: each textured
67 /// `IfcTriangulatedFaceSet` item becomes its OWN part carrying its UVs +
68 /// decoded image (so a representation with several differently-textured
69 /// items renders each with the correct image), and all untextured items are
70 /// merged into a single part with empty UVs / no texture. The MappingOrigin
71 /// placement is baked into every part.
72 pub fn process_representation_map_with_texture(
73 &self,
74 rep_map: &DecodedEntity,
75 decoder: &mut EntityDecoder,
76 texture_index: &rustc_hash::FxHashMap<u32, crate::processors::texture::ResolvedTextureMap>,
77 ) -> Result<
78 Vec<(
79 Mesh,
80 Vec<f32>,
81 Option<crate::processors::texture::TextureAttachment>,
82 )>,
83 > {
84 // attr 1: MappedRepresentation (IfcShapeRepresentation)
85 let mapped_rep_attr = rep_map.get(1).ok_or_else(|| {
86 Error::geometry("RepresentationMap missing MappedRepresentation".to_string())
87 })?;
88 let mapped_rep = decoder
89 .resolve_ref(mapped_rep_attr)?
90 .ok_or_else(|| Error::geometry("Failed to resolve MappedRepresentation".to_string()))?;
91
92 // attr 3: Items
93 let items_attr = mapped_rep
94 .get(3)
95 .ok_or_else(|| Error::geometry("Representation missing Items".to_string()))?;
96 let items = decoder.resolve_ref_list(items_attr)?;
97
98 let mut untextured = Mesh::new();
99 // One entry per textured item — keeps each item with its own image.
100 let mut textured: Vec<(
101 Mesh,
102 Vec<f32>,
103 crate::processors::texture::TextureAttachment,
104 )> = Vec::new();
105 for item in items {
106 // A nested IfcMappedItem inside a type's own representation: process
107 // it (applies its MappingTarget) rather than dropping its geometry.
108 if item.ifc_type == IfcType::IfcMappedItem {
109 if let Ok(sub_mesh) = self.process_mapped_item_cached(&item, decoder) {
110 untextured.merge(&sub_mesh); // already scaled inside the cached path
111 }
112 continue;
113 }
114
115 // Textured tessellated face set → its own part with per-vertex UVs (#961).
116 if item.ifc_type == IfcType::IfcTriangulatedFaceSet {
117 if let Some(map) = texture_index.get(&item.id) {
118 let proc = crate::processors::TriangulatedFaceSetProcessor::new();
119 if let Ok((mut sub_mesh, sub_uvs)) =
120 proc.process_with_texture(&item, decoder, map)
121 {
122 self.scale_mesh(&mut sub_mesh); // UVs are unaffected by scale
123 textured.push((sub_mesh, sub_uvs, map.attachment()));
124 continue;
125 }
126 }
127 }
128
129 if let Some(processor) = self.processors.get(&item.ifc_type) {
130 if let Ok(mut sub_mesh) =
131 processor.process(&item, decoder, &self.schema, self.tessellation_quality)
132 {
133 sub_mesh.validate_indices();
134 self.scale_mesh(&mut sub_mesh);
135 untextured.merge(&sub_mesh);
136 }
137 }
138 }
139
140 // attr 0: MappingOrigin (IfcAxis2Placement3D) — the only 3D transform;
141 // UVs are 2D and unaffected. Parse once, bake into every part.
142 let origin_transform: Option<nalgebra::Matrix4<f64>> = match rep_map.get(0) {
143 Some(origin_attr) if !origin_attr.is_null() => {
144 match decoder.resolve_ref(origin_attr)? {
145 Some(origin) if origin.ifc_type == IfcType::IfcAxis2Placement3D => {
146 let mut t = self.parse_axis2_placement_3d(&origin, decoder)?;
147 self.scale_transform(&mut t);
148 Some(t)
149 }
150 _ => None,
151 }
152 }
153 _ => None,
154 };
155
156 let mut out: Vec<(
157 Mesh,
158 Vec<f32>,
159 Option<crate::processors::texture::TextureAttachment>,
160 )> = Vec::new();
161 for (mut mesh, uvs, texture) in textured {
162 if let Some(t) = &origin_transform {
163 self.transform_mesh_local(&mut mesh, t);
164 }
165 // Same sliver hygiene as the other mesh-output chokepoints. This is
166 // the type-geometry (RepresentationMap) channel and the only one
167 // carrying a parallel per-vertex UV array; clean_degenerate edits
168 // only indices (vertices/UVs untouched), so the UVs stay in sync.
169 mesh.clean_degenerate();
170 out.push((mesh, uvs, Some(texture)));
171 }
172 if !untextured.is_empty() {
173 if let Some(t) = &origin_transform {
174 self.transform_mesh_local(&mut untextured, t);
175 }
176 untextured.clean_degenerate();
177 out.push((untextured, Vec::new(), None));
178 }
179
180 Ok(out)
181 }
182}