Skip to main content

ifc_lite_wasm/api/
simplify.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//! Demesher wasm surface (`simplifyMeshes`).
6//!
7//! Simplifies already-produced element meshes (the `MeshData` the consumer
8//! holds from the normal load) at a per-element demesher level and returns
9//! BOTH render-ready replacement meshes (boundary Y-up convention, ready for
10//! `Scene.addMeshes`) and the same triangles in each element's IFC
11//! object-placement frame in file units (for the tessellated IFC re-export).
12//! Mesh-domain on purpose: a button press must not re-parse the model, and
13//! the #1474 placement capture needed for the inverse transform rides on the
14//! meshes themselves (`localToWorld`).
15
16use crate::api::IfcAPI;
17use ifc_lite_processing::simplify_session::{simplify_element, SimplifyRecordInput, SimplifySkip};
18use wasm_bindgen::prelude::*;
19
20/// Flat result of `simplifyMeshes`: per surviving element `i`,
21/// `vertexCounts[i]` vertices and `indexCounts[i]` indices taken in order
22/// from the concatenated arrays (mirrors the `exportGlbFromMeshes` wire
23/// convention). `localPositions` is 1:1 with `renderPositions` (f64, file
24/// units, element object frame); `localIndices` is 1:1 with `renderIndices`
25/// but in the IFC frame's winding. Skipped elements are reported in
26/// `skippedIds` / `skippedReasons` and must keep their original geometry.
27#[wasm_bindgen]
28pub struct SimplifiedMeshes {
29    element_ids: Vec<u32>,
30    levels: Vec<u8>,
31    vertex_counts: Vec<u32>,
32    index_counts: Vec<u32>,
33    render_positions: Vec<f32>,
34    render_normals: Vec<f32>,
35    render_indices: Vec<u32>,
36    render_origins: Vec<f64>,
37    local_positions: Vec<f64>,
38    local_indices: Vec<u32>,
39    tris_before: Vec<u32>,
40    tris_after: Vec<u32>,
41    cavities_dropped: Vec<u32>,
42    skipped_ids: Vec<u32>,
43    skipped_reasons: Vec<JsValue>,
44}
45
46#[wasm_bindgen]
47impl SimplifiedMeshes {
48    #[wasm_bindgen(getter, js_name = elementIds)]
49    pub fn element_ids(&self) -> Vec<u32> {
50        self.element_ids.clone()
51    }
52
53    #[wasm_bindgen(getter)]
54    pub fn levels(&self) -> Vec<u8> {
55        self.levels.clone()
56    }
57
58    #[wasm_bindgen(getter, js_name = vertexCounts)]
59    pub fn vertex_counts(&self) -> Vec<u32> {
60        self.vertex_counts.clone()
61    }
62
63    #[wasm_bindgen(getter, js_name = indexCounts)]
64    pub fn index_counts(&self) -> Vec<u32> {
65        self.index_counts.clone()
66    }
67
68    #[wasm_bindgen(getter, js_name = renderPositions)]
69    pub fn render_positions(&self) -> Vec<f32> {
70        self.render_positions.clone()
71    }
72
73    #[wasm_bindgen(getter, js_name = renderNormals)]
74    pub fn render_normals(&self) -> Vec<f32> {
75        self.render_normals.clone()
76    }
77
78    #[wasm_bindgen(getter, js_name = renderIndices)]
79    pub fn render_indices(&self) -> Vec<u32> {
80        self.render_indices.clone()
81    }
82
83    /// xyz per element (frame matches the render positions' convention).
84    #[wasm_bindgen(getter, js_name = renderOrigins)]
85    pub fn render_origins(&self) -> Vec<f64> {
86        self.render_origins.clone()
87    }
88
89    #[wasm_bindgen(getter, js_name = localPositions)]
90    pub fn local_positions(&self) -> Vec<f64> {
91        self.local_positions.clone()
92    }
93
94    #[wasm_bindgen(getter, js_name = localIndices)]
95    pub fn local_indices(&self) -> Vec<u32> {
96        self.local_indices.clone()
97    }
98
99    #[wasm_bindgen(getter, js_name = trisBefore)]
100    pub fn tris_before(&self) -> Vec<u32> {
101        self.tris_before.clone()
102    }
103
104    #[wasm_bindgen(getter, js_name = trisAfter)]
105    pub fn tris_after(&self) -> Vec<u32> {
106        self.tris_after.clone()
107    }
108
109    #[wasm_bindgen(getter, js_name = cavitiesDropped)]
110    pub fn cavities_dropped(&self) -> Vec<u32> {
111        self.cavities_dropped.clone()
112    }
113
114    #[wasm_bindgen(getter, js_name = skippedIds)]
115    pub fn skipped_ids(&self) -> Vec<u32> {
116        self.skipped_ids.clone()
117    }
118
119    /// Skip reason per `skippedIds` entry (stable slugs:
120    /// `no-geometry` / `missing-placement` / `singular-placement` /
121    /// `empty-result` / `invalid-unit-scale`).
122    #[wasm_bindgen(getter, js_name = skippedReasons)]
123    pub fn skipped_reasons(&self) -> Vec<JsValue> {
124        self.skipped_reasons.clone()
125    }
126}
127
128#[wasm_bindgen]
129impl IfcAPI {
130    /// Simplify already-produced element meshes at per-element demesher
131    /// levels (1-4 = cavity removal + clustering at 0.5/0.25/0.10/0.03
132    /// triangle ratio, 5 = bounding box).
133    ///
134    /// One RECORD per input `MeshData` entry (an element may span several
135    /// records — per-material submeshes; pass all of them, grouped or not).
136    /// Per record `i`: `vertexCounts[i]` vertices from `positions` (and
137    /// `normals` when non-empty), `indexCounts[i]` indices from `indices`
138    /// (per-record local), `origins[i*3..]`, `localToWorld[i*16..]` valid
139    /// only when `localToWorldPresent[i] != 0`, level `levels[i]` (records
140    /// of one element must agree). Arrays are the boundary Y-up convention
141    /// when `yUp` is true (the browser/SDK case).
142    ///
143    /// `rtcX/Y/Z` = `coordinateInfo.originShift` (IFC Z-up metres);
144    /// `unitScale` = metres per project length unit.
145    #[wasm_bindgen(js_name = simplifyMeshes)]
146    #[allow(clippy::too_many_arguments)]
147    pub fn simplify_meshes(
148        &self,
149        express_ids: &[u32],
150        levels: &[u8],
151        positions: &[f32],
152        normals: &[f32],
153        indices: &[u32],
154        vertex_counts: &[u32],
155        index_counts: &[u32],
156        origins: &[f64],
157        local_to_world: &[f64],
158        local_to_world_present: &[u8],
159        rtc_x: f64,
160        rtc_y: f64,
161        rtc_z: f64,
162        unit_scale: f64,
163        y_up: bool,
164    ) -> Result<SimplifiedMeshes, JsValue> {
165        let n = express_ids.len();
166        if levels.len() != n
167            || vertex_counts.len() != n
168            || index_counts.len() != n
169            || origins.len() != n * 3
170            || local_to_world.len() != n * 16
171            || local_to_world_present.len() != n
172        {
173            return Err(
174                js_sys::Error::new("simplifyMeshes: per-record array lengths disagree").into(),
175            );
176        }
177        let has_normals = !normals.is_empty();
178        if has_normals && normals.len() != positions.len() {
179            return Err(js_sys::Error::new(
180                "simplifyMeshes: normals must be empty or 1:1 with positions",
181            )
182            .into());
183        }
184
185        // Slice the concatenated arrays into per-record views, grouped by
186        // element in first-seen order.
187        // Accumulate offsets in u64: on wasm32 `usize` is 32-bit, so a hostile
188        // `vertexCounts` entry near u32::MAX would overflow `count * 3` /
189        // the running sum and could slip past the bounds check below.
190        let mut order: Vec<u32> = Vec::new();
191        let mut groups: rustc_hash::FxHashMap<u32, Vec<usize>> = rustc_hash::FxHashMap::default();
192        let mut pos_offsets: Vec<u64> = Vec::with_capacity(n);
193        let mut idx_offsets: Vec<u64> = Vec::with_capacity(n);
194        let (mut pos_off, mut idx_off) = (0u64, 0u64);
195        for i in 0..n {
196            pos_offsets.push(pos_off);
197            idx_offsets.push(idx_off);
198            pos_off += vertex_counts[i] as u64 * 3;
199            idx_off += index_counts[i] as u64;
200            if !groups.contains_key(&express_ids[i]) {
201                order.push(express_ids[i]);
202            }
203            groups.entry(express_ids[i]).or_default().push(i);
204        }
205        // Exact totals: trailing unaccounted positions/indices are a malformed
206        // wire payload, not slack to ignore.
207        if pos_off != positions.len() as u64 || idx_off != indices.len() as u64 {
208            return Err(js_sys::Error::new(
209                "simplifyMeshes: counts do not match concatenated array lengths",
210            )
211            .into());
212        }
213
214        let mut out = SimplifiedMeshes {
215            element_ids: Vec::new(),
216            levels: Vec::new(),
217            vertex_counts: Vec::new(),
218            index_counts: Vec::new(),
219            render_positions: Vec::new(),
220            render_normals: Vec::new(),
221            render_indices: Vec::new(),
222            render_origins: Vec::new(),
223            local_positions: Vec::new(),
224            local_indices: Vec::new(),
225            tris_before: Vec::new(),
226            tris_after: Vec::new(),
227            cavities_dropped: Vec::new(),
228            skipped_ids: Vec::new(),
229            skipped_reasons: Vec::new(),
230        };
231
232        for id in order {
233            let record_indices = &groups[&id];
234            let level = levels[record_indices[0]];
235            if record_indices.iter().any(|&i| levels[i] != level) {
236                return Err(js_sys::Error::new(&format!(
237                    "simplifyMeshes: records for element {id} have conflicting levels"
238                ))
239                .into());
240            }
241            let records: Vec<SimplifyRecordInput<'_>> = record_indices
242                .iter()
243                .map(|&i| {
244                    // In-bounds by the total check above, so the u64->usize
245                    // casts cannot truncate.
246                    let (po, pn) = (
247                        pos_offsets[i] as usize,
248                        (vertex_counts[i] as u64 * 3) as usize,
249                    );
250                    let io = idx_offsets[i] as usize;
251                    let pos = &positions[po..po + pn];
252                    let nrm = if has_normals {
253                        &normals[po..po + pn]
254                    } else {
255                        &[][..]
256                    };
257                    let idx = &indices[io..io + index_counts[i] as usize];
258                    let l2w = if local_to_world_present[i] != 0 {
259                        let mut m = [0.0f64; 16];
260                        m.copy_from_slice(&local_to_world[i * 16..i * 16 + 16]);
261                        Some(m)
262                    } else {
263                        None
264                    };
265                    SimplifyRecordInput {
266                        positions: pos,
267                        normals: nrm,
268                        indices: idx,
269                        origin: [origins[i * 3], origins[i * 3 + 1], origins[i * 3 + 2]],
270                        local_to_world: l2w,
271                    }
272                })
273                .collect();
274
275            match simplify_element(&records, level, [rtc_x, rtc_y, rtc_z], unit_scale, y_up) {
276                Ok(res) => {
277                    out.element_ids.push(id);
278                    out.levels.push(level);
279                    out.vertex_counts
280                        .push((res.render_positions.len() / 3) as u32);
281                    out.index_counts.push(res.render_indices.len() as u32);
282                    out.render_positions
283                        .extend_from_slice(&res.render_positions);
284                    out.render_normals.extend_from_slice(&res.render_normals);
285                    out.render_indices.extend_from_slice(&res.render_indices);
286                    out.render_origins.extend_from_slice(&res.render_origin);
287                    out.local_positions.extend_from_slice(&res.local_positions);
288                    out.local_indices.extend_from_slice(&res.local_indices);
289                    out.tris_before.push(res.tris_before);
290                    out.tris_after.push(res.tris_after);
291                    out.cavities_dropped.push(res.cavity_components_dropped);
292                }
293                Err(skip) => {
294                    out.skipped_ids.push(id);
295                    out.skipped_reasons.push(JsValue::from_str(skip.as_str()));
296                    if matches!(skip, SimplifySkip::EmptyResult) {
297                        // Unexpected: worth a console breadcrumb, not a failure.
298                        web_sys::console::warn_1(&JsValue::from_str(&format!(
299                            "[ifc-lite] simplifyMeshes: element {id} produced an empty result; kept original"
300                        )));
301                    }
302                }
303            }
304        }
305
306        Ok(out)
307    }
308}