Skip to main content

ifc_lite_processing/
simplify_session.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 session core: simplify already-produced element meshes and hand
6//! back BOTH a render-ready replacement mesh and the same vertices in the
7//! element's IFC object-placement frame (file units) for tessellated IFC
8//! re-export.
9//!
10//! Operates on the meshes the consumer already holds (the viewer's / SDK's
11//! `MeshData`), NOT on file bytes: a button press must not re-parse and
12//! re-mesh the whole model, and the placement capture the inverse transform
13//! needs (`origin`, `local_to_world`, #1474) rides on those meshes.
14//!
15//! Frames: consumer meshes arrive in the wasm boundary convention — WebGL
16//! Y-up positions/normals/origin, winding reversed, `local_to_world`
17//! conjugated (`zero_copy::mesh::MeshDataJs::new`) — with `y_up = true`;
18//! native in-memory meshes (IFC Z-up, untouched winding) pass `y_up = false`.
19//! `rtc_offset` is the model's origin shift in IFC Z-up metres
20//! (`coordinateInfo.originShift`); reconstruction per vertex is
21//! `true_world = origin + position + rtc_offset` in the Z-up frame, then
22//! `local = inv(local_to_world) * true_world`, then `/ unit_scale` into file
23//! units.
24
25use crate::simplify_math::{
26    averaged_vertex_normals, conjugate_yup_to_zup, invert_affine_row_major,
27    transform_point_row_major, yup_to_zup, zup_to_yup,
28};
29use ifc_lite_geometry::simplify::{simplify_mesh, SimplifyOptions};
30use ifc_lite_geometry::Mesh;
31
32/// One already-produced mesh record of an element (an element may carry
33/// several: per-material submesh splits).
34#[derive(Debug, Clone)]
35pub struct SimplifyRecordInput<'a> {
36    /// Vertex positions relative to `origin` (frame per `y_up`).
37    pub positions: &'a [f32],
38    /// Vertex normals, 1:1 with positions (may be empty).
39    pub normals: &'a [f32],
40    /// Triangle indices (winding per `y_up`).
41    pub indices: &'a [u32],
42    /// Per-mesh local origin (frame per `y_up`); world = origin + position.
43    pub origin: [f64; 3],
44    /// Resolved placement chain (row-major; conjugated when `y_up`), see
45    /// `Mesh::local_to_world`. Required on at least one record.
46    pub local_to_world: Option<[f64; 16]>,
47}
48
49/// Why an element could not be simplified. The caller keeps the original
50/// geometry for these — a skip is never destructive.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum SimplifySkip {
53    /// No records / no triangles.
54    NoGeometry,
55    /// No record carries the #1474 placement capture (synthetic meshes, or a
56    /// consumer that dropped `localToWorld`); the IFC-local frame cannot be
57    /// reconstructed.
58    MissingPlacement,
59    /// The placement matrix is not invertible.
60    SingularPlacement,
61    /// Simplification emptied the mesh (should not happen; guarded anyway —
62    /// an element must never disappear).
63    EmptyResult,
64    /// `unit_scale` is zero, negative or non-finite. Silently assuming
65    /// metres would export tessellation at the wrong size while reporting
66    /// success, so the element is skipped instead.
67    InvalidUnitScale,
68}
69
70impl SimplifySkip {
71    pub fn as_str(&self) -> &'static str {
72        match self {
73            SimplifySkip::NoGeometry => "no-geometry",
74            SimplifySkip::MissingPlacement => "missing-placement",
75            SimplifySkip::SingularPlacement => "singular-placement",
76            SimplifySkip::EmptyResult => "empty-result",
77            SimplifySkip::InvalidUnitScale => "invalid-unit-scale",
78        }
79    }
80}
81
82/// Simplified element: one render mesh (input frame convention) + the same
83/// triangles in the element's IFC object-placement frame in file units.
84#[derive(Debug, Clone)]
85pub struct SimplifiedElement {
86    /// Render positions relative to `render_origin` (frame per `y_up`).
87    pub render_positions: Vec<f32>,
88    pub render_normals: Vec<f32>,
89    /// Render indices (winding per `y_up`).
90    pub render_indices: Vec<u32>,
91    /// Per-mesh origin for the render positions (frame per `y_up`).
92    pub render_origin: [f64; 3],
93    /// The same vertices in the element's object-placement frame, FILE units,
94    /// IFC Z-up. 1:1 with `render_positions` triplets.
95    pub local_positions: Vec<f64>,
96    /// Triangle indices over `local_positions` in the IFC frame's winding
97    /// (counter-clockwise outward).
98    pub local_indices: Vec<u32>,
99    pub tris_before: u32,
100    pub tris_after: u32,
101    pub cavity_components_dropped: u32,
102}
103
104/// Simplify one element from its already-produced mesh records at the given
105/// demesher level (see `SimplifyOptions::for_level`).
106///
107/// `unit_scale` is metres per project length unit (the prepass/export unit
108/// scale); `rtc_offset` is the model origin shift in IFC Z-up metres.
109pub fn simplify_element(
110    records: &[SimplifyRecordInput<'_>],
111    level: u8,
112    rtc_offset: [f64; 3],
113    unit_scale: f64,
114    y_up: bool,
115) -> Result<SimplifiedElement, SimplifySkip> {
116    // -- Placement: first record that carries the capture (submeshes of one
117    // element share the element's placement chain).
118    let l2w_raw = records
119        .iter()
120        .find_map(|r| r.local_to_world)
121        .ok_or(SimplifySkip::MissingPlacement)?;
122    let l2w = if y_up {
123        conjugate_yup_to_zup(&l2w_raw)
124    } else {
125        l2w_raw
126    };
127    let inv_l2w = invert_affine_row_major(&l2w).ok_or(SimplifySkip::SingularPlacement)?;
128    if !(unit_scale.is_finite() && unit_scale > 0.0) {
129        return Err(SimplifySkip::InvalidUnitScale);
130    }
131
132    // -- Merge records into one IFC Z-up soup in the RTC-shifted world frame
133    // (f64), restoring the IFC winding when the input is the Y-up boundary
134    // convention.
135    let mut world: Vec<[f64; 3]> = Vec::new();
136    let mut normals: Vec<f32> = Vec::new();
137    let mut indices: Vec<u32> = Vec::new();
138    let mut have_normals = true;
139    for rec in records {
140        let base = world.len() as u32;
141        let n_verts = rec.positions.len() / 3;
142        let origin = if y_up {
143            yup_to_zup(rec.origin)
144        } else {
145            rec.origin
146        };
147        for chunk in rec.positions.chunks_exact(3) {
148            let p = [chunk[0] as f64, chunk[1] as f64, chunk[2] as f64];
149            let p = if y_up { yup_to_zup(p) } else { p };
150            world.push([p[0] + origin[0], p[1] + origin[1], p[2] + origin[2]]);
151        }
152        if rec.normals.len() == rec.positions.len() {
153            for chunk in rec.normals.chunks_exact(3) {
154                let n = [chunk[0] as f64, chunk[1] as f64, chunk[2] as f64];
155                let n = if y_up { yup_to_zup(n) } else { n };
156                normals.extend_from_slice(&[n[0] as f32, n[1] as f32, n[2] as f32]);
157            }
158        } else {
159            have_normals = false;
160        }
161        for tri in rec.indices.chunks_exact(3) {
162            if (tri[0] as usize) >= n_verts
163                || (tri[1] as usize) >= n_verts
164                || (tri[2] as usize) >= n_verts
165            {
166                continue;
167            }
168            // The boundary reversed winding for the Y-up handedness flip;
169            // restore the IFC order for frame-consistent processing.
170            if y_up {
171                indices.extend_from_slice(&[tri[0] + base, tri[2] + base, tri[1] + base]);
172            } else {
173                indices.extend_from_slice(&[tri[0] + base, tri[1] + base, tri[2] + base]);
174            }
175        }
176    }
177    if world.is_empty() || indices.is_empty() {
178        return Err(SimplifySkip::NoGeometry);
179    }
180
181    // -- Rebase to the element AABB centre so f32 mesh positions stay small
182    // and precise at building/georef scale (same trick as the pipeline's
183    // per-mesh `origin`).
184    let mut min = [f64::INFINITY; 3];
185    let mut max = [f64::NEG_INFINITY; 3];
186    for w in &world {
187        for k in 0..3 {
188            min[k] = min[k].min(w[k]);
189            max[k] = max[k].max(w[k]);
190        }
191    }
192    let centre = [
193        0.5 * (min[0] + max[0]),
194        0.5 * (min[1] + max[1]),
195        0.5 * (min[2] + max[2]),
196    ];
197    let mut mesh = Mesh::new();
198    mesh.positions = world
199        .iter()
200        .flat_map(|w| {
201            [
202                (w[0] - centre[0]) as f32,
203                (w[1] - centre[1]) as f32,
204                (w[2] - centre[2]) as f32,
205            ]
206        })
207        .collect();
208    mesh.normals = if have_normals && normals.len() == mesh.positions.len() {
209        normals
210    } else {
211        Vec::new()
212    };
213    mesh.indices = indices;
214    mesh.origin = centre;
215    mesh.local_to_world = Some(l2w);
216
217    // -- Simplify.
218    let (mut out, stats) = simplify_mesh(&mesh, &SimplifyOptions::for_level(level));
219    if out.indices.is_empty() || out.positions.is_empty() {
220        return Err(SimplifySkip::EmptyResult);
221    }
222    if out.normals.len() != out.positions.len() {
223        out.normals = averaged_vertex_normals(&out.positions, &out.indices);
224    }
225
226    // -- IFC-local output: true world -> inverse placement -> file units.
227    let n_out = out.positions.len() / 3;
228    let mut local_positions: Vec<f64> = Vec::with_capacity(n_out * 3);
229    for chunk in out.positions.chunks_exact(3) {
230        let tw = [
231            chunk[0] as f64 + out.origin[0] + rtc_offset[0],
232            chunk[1] as f64 + out.origin[1] + rtc_offset[1],
233            chunk[2] as f64 + out.origin[2] + rtc_offset[2],
234        ];
235        let local = transform_point_row_major(&inv_l2w, tw);
236        local_positions.extend_from_slice(&[
237            local[0] / unit_scale,
238            local[1] / unit_scale,
239            local[2] / unit_scale,
240        ]);
241    }
242    let local_indices = out.indices.clone();
243
244    // -- Render output back in the caller's frame convention.
245    let (render_positions, render_normals, render_indices, render_origin) = if y_up {
246        let positions = out
247            .positions
248            .chunks_exact(3)
249            .flat_map(|c| {
250                let p = zup_to_yup([c[0] as f64, c[1] as f64, c[2] as f64]);
251                [p[0] as f32, p[1] as f32, p[2] as f32]
252            })
253            .collect();
254        let normals = out
255            .normals
256            .chunks_exact(3)
257            .flat_map(|c| {
258                let n = zup_to_yup([c[0] as f64, c[1] as f64, c[2] as f64]);
259                [n[0] as f32, n[1] as f32, n[2] as f32]
260            })
261            .collect();
262        let mut indices = out.indices.clone();
263        for tri in indices.chunks_exact_mut(3) {
264            tri.swap(1, 2);
265        }
266        (positions, normals, indices, zup_to_yup(out.origin))
267    } else {
268        (out.positions, out.normals, out.indices, out.origin)
269    };
270
271    Ok(SimplifiedElement {
272        render_positions,
273        render_normals,
274        render_indices,
275        render_origin,
276        local_positions,
277        local_indices,
278        tris_before: stats.tris_before,
279        tris_after: stats.tris_after,
280        cavity_components_dropped: stats.cavity_components_dropped,
281    })
282}
283