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;