ifc_lite_processing/style/finish_join.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//! #5984: the IFC-authored finish (#5582) of an already PRODUCED mesh.
6//!
7//! `MeshData` carries no finish (a new pub field on it is a semver major), so
8//! every consumer that gets meshes after production joins the finish on
9//! afterwards: the wasm batch, its instanced (IFNS) occurrences, and the
10//! server's JSON and Parquet transports. They all join here, once.
11//!
12//! THE RULE: a mesh's finish comes from the style its colour came from, never
13//! from another. So the join asks the colour walks themselves which styled
14//! item the colour was taken from (`crate::element`'s style-source walks), in
15//! the same precedence mesh production applies (`resolve_submesh_color`):
16//!
17//! 1. **Item level.** The mesh's representation item, directly styled or
18//! reaching a styled item through the bounded `IfcMappedItem` chase
19//! (`find_geometry_item_color`). A hit decides: the finish is that item's,
20//! or none when its style authors none (a fill-only style).
21//! 2. **Element level**, when the item resolved nothing and the colour fell
22//! back to the element's: the element's own style source. For a product
23//! that is its `IfcProductDefinitionShape` walk; for a type product (#957
24//! type geometry, no item id on its meshes) each `IfcRepresentationMap` in
25//! order, which is where a finish authored only on a type's
26//! representation map is reached. A fallback candidate is accepted only
27//! when its style's colour IS the mesh's colour: the element-level colour
28//! competes with the material chain and the type default, both colour-only,
29//! and the mesh does not record which one won. The equality is what tells
30//! them apart, and for a type with several maps it is what pairs each mesh
31//! with its own map. Its one blind spot is a material colour bit-identical
32//! to the element's style colour, which renders identically anyway.
33//!
34//! `finishes` is [`crate::prepass::resolve_geometry_finishes`]'s index (the
35//! wasm host's `setStyleFinishes` wire decodes to the same map), keyed by
36//! styled geometry item. `style_color` is the colour index's lookup by the
37//! same key: its membership decides where each walk stops, exactly as the
38//! colour walks stop, and its colour is what the fallback compares against.
39
40use crate::prepass::{resolve_geometry_finishes, PrepassSpans};
41use crate::style::{GeometryStyleInfo, SpecularMaterial};
42use crate::types::mesh::MeshData;
43use ifc_lite_core::{EntityDecoder, EntityIndex, EntityScanner, IfcType};
44use rustc_hash::FxHashMap;
45
46/// Element id → its element-level style-source candidates, in colour order.
47type ElementSources = FxHashMap<u32, Vec<u32>>;
48
49/// The join itself (see the module doc for the rule). `element_sources`
50/// memoises step 2 per element: a type product or a multi-part product has
51/// many meshes and one set of candidates.
52fn join_finish(
53 express_id: u32,
54 geometry_item_id: Option<u32>,
55 color: [f32; 4],
56 style_color: &dyn Fn(u32) -> Option<[f32; 4]>,
57 finishes: &FxHashMap<u32, SpecularMaterial>,
58 element_sources: &mut ElementSources,
59 decoder: &mut EntityDecoder,
60) -> Option<SpecularMaterial> {
61 if finishes.is_empty() {
62 return None;
63 }
64 let has_style = |id: u32| style_color(id).is_some();
65 let finish_of = |source: u32| {
66 finishes
67 .get(&source)
68 .copied()
69 .filter(|f| f.metallic.is_some() || f.roughness.is_some())
70 };
71 if let Some(item) = geometry_item_id {
72 if let Some(source) = crate::element::find_geometry_item_style_source(item, &has_style, decoder) {
73 return finish_of(source);
74 }
75 }
76 let candidates = element_sources
77 .entry(express_id)
78 .or_insert_with(|| element_style_sources(express_id, &has_style, decoder));
79 candidates
80 .iter()
81 .find(|&&source| style_color(source) == Some(color))
82 .and_then(|&source| finish_of(source))
83}
84
85/// Step 2's candidates for one element: every representation map of a type
86/// product (in `RepresentationMaps` order, as `produce_type_geometry` renders
87/// them), or the one product-shape source of a product. Attribute 6 is
88/// `RepresentationMaps` on `IfcTypeProduct` and `Representation` on
89/// `IfcProduct`.
90fn element_style_sources(
91 express_id: u32,
92 has_style: &dyn Fn(u32) -> bool,
93 decoder: &mut EntityDecoder,
94) -> Vec<u32> {
95 let Ok(entity) = decoder.decode_by_id(express_id) else {
96 return Vec::new();
97 };
98 if entity.ifc_type.is_subtype_of(IfcType::IfcTypeProduct) {
99 return entity
100 .get_refs(6)
101 .unwrap_or_default()
102 .into_iter()
103 .filter_map(|rep_map| crate::element::representation_map_style_source(rep_map, has_style, decoder))
104 .collect();
105 }
106 entity
107 .get_ref(6)
108 .and_then(|shape| crate::element::product_shape_style_source(shape, has_style, decoder))
109 .into_iter()
110 .collect()
111}
112
113/// The finish join over caller-held indexes: the wasm batch, which already
114/// holds its colour index (as `id -> rgba`) and the installed finishes, and
115/// decodes through its own per-batch decoder.
116pub struct MeshFinishJoin<'a> {
117 finishes: &'a FxHashMap<u32, SpecularMaterial>,
118 style_color: Box<dyn Fn(u32) -> Option<[f32; 4]> + 'a>,
119 element_sources: ElementSources,
120}
121
122impl<'a> MeshFinishJoin<'a> {
123 /// `style_color(id)` is the colour index's entry for styled geometry
124 /// item `id` (`None` when it has no style).
125 pub fn new(
126 finishes: &'a FxHashMap<u32, SpecularMaterial>,
127 style_color: impl Fn(u32) -> Option<[f32; 4]> + 'a,
128 ) -> Self {
129 Self { finishes, style_color: Box::new(style_color), element_sources: ElementSources::default() }
130 }
131
132 /// The finish of the mesh (or instanced occurrence) with this element,
133 /// source item and colour. `None` when nothing authored one.
134 pub fn finish(
135 &mut self,
136 express_id: u32,
137 geometry_item_id: Option<u32>,
138 color: [f32; 4],
139 decoder: &mut EntityDecoder,
140 ) -> Option<SpecularMaterial> {
141 join_finish(
142 express_id,
143 geometry_item_id,
144 color,
145 &*self.style_color,
146 self.finishes,
147 &mut self.element_sources,
148 decoder,
149 )
150 }
151
152 /// [`Self::finish`] for a produced mesh.
153 pub fn finish_for_mesh(&mut self, mesh: &MeshData, decoder: &mut EntityDecoder) -> Option<SpecularMaterial> {
154 self.finish(mesh.express_id, mesh.geometry_item_id, mesh.color, decoder)
155 }
156}
157
158/// The finish join resolved from a file's bytes alone, for a consumer that
159/// holds nothing but the file and its produced meshes (the server's
160/// transports). One scan collects the styled items and the entity index; the
161/// colour index is the pipeline's own (`resolve_styled_items_into`, every
162/// styled item resolved, as the finished pipeline has them) and the finish
163/// index is the one the wasm prepass ships.
164pub struct ModelFinishes<'c> {
165 decoder: EntityDecoder<'c>,
166 geometry_styles: FxHashMap<u32, GeometryStyleInfo>,
167 finishes: FxHashMap<u32, SpecularMaterial>,
168 element_sources: ElementSources,
169}
170
171impl<'c> ModelFinishes<'c> {
172 pub fn from_content(content: &'c [u8]) -> Self {
173 let mut spans = PrepassSpans::default();
174 let mut index = EntityIndex::default();
175 let mut scanner = EntityScanner::new(content);
176 while let Some((id, type_name, start, end)) = scanner.next_entity() {
177 index.insert(id, (start, end));
178 spans.stash(type_name, id, start, end);
179 }
180 let mut decoder = EntityDecoder::with_index(content, index);
181 let finishes = resolve_geometry_finishes(&spans.styled_items, &mut decoder);
182 let mut geometry_styles = FxHashMap::default();
183 if !finishes.is_empty() {
184 crate::prepass::resolve_styled_items_into(
185 &spans.styled_items,
186 &mut decoder,
187 false,
188 &mut FxHashMap::default(),
189 &mut geometry_styles,
190 &mut Vec::new(),
191 );
192 }
193 Self { decoder, geometry_styles, finishes, element_sources: ElementSources::default() }
194 }
195
196 /// Whether the file authors no finish at all (every join is then `None`).
197 pub fn is_empty(&self) -> bool {
198 self.finishes.is_empty()
199 }
200
201 /// The finish `mesh` carries, by the module's rule.
202 pub fn finish_for_mesh(&mut self, mesh: &MeshData) -> Option<SpecularMaterial> {
203 let styles = &self.geometry_styles;
204 join_finish(
205 mesh.express_id,
206 mesh.geometry_item_id,
207 mesh.color,
208 &|id| styles.get(&id).map(|s| s.color),
209 &self.finishes,
210 &mut self.element_sources,
211 &mut self.decoder,
212 )
213 }
214}
215
216#[cfg(test)]
217#[path = "finish_join_tests.rs"]
218mod tests;