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 preserved, `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 (both frames preserve outward winding).
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 (both frames preserve outward winding).
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). The Y-up conversion is a proper rotation, preserving winding.
134    let mut world: Vec<[f64; 3]> = Vec::new();
135    let mut normals: Vec<f32> = Vec::new();
136    let mut indices: Vec<u32> = Vec::new();
137    let mut have_normals = true;
138    for rec in records {
139        let base = world.len() as u32;
140        let n_verts = rec.positions.len() / 3;
141        let origin = if y_up {
142            yup_to_zup(rec.origin)
143        } else {
144            rec.origin
145        };
146        for chunk in rec.positions.chunks_exact(3) {
147            let p = [chunk[0] as f64, chunk[1] as f64, chunk[2] as f64];
148            let p = if y_up { yup_to_zup(p) } else { p };
149            world.push([p[0] + origin[0], p[1] + origin[1], p[2] + origin[2]]);
150        }
151        if rec.normals.len() == rec.positions.len() {
152            for chunk in rec.normals.chunks_exact(3) {
153                let n = [chunk[0] as f64, chunk[1] as f64, chunk[2] as f64];
154                let n = if y_up { yup_to_zup(n) } else { n };
155                normals.extend_from_slice(&[n[0] as f32, n[1] as f32, n[2] as f32]);
156            }
157        } else {
158            have_normals = false;
159        }
160        for tri in rec.indices.chunks_exact(3) {
161            if (tri[0] as usize) >= n_verts
162                || (tri[1] as usize) >= n_verts
163                || (tri[2] as usize) >= n_verts
164            {
165                continue;
166            }
167            // #4056: (x,y,z) -> (x,z,-y) has determinant +1 in either
168            // direction, so the same index order is valid in both frames.
169            indices.extend_from_slice(&[tri[0] + base, tri[1] + base, tri[2] + base]);
170        }
171    }
172    if world.is_empty() || indices.is_empty() {
173        return Err(SimplifySkip::NoGeometry);
174    }
175
176    // -- Rebase to the element AABB centre so f32 mesh positions stay small
177    // and precise at building/georef scale (same trick as the pipeline's
178    // per-mesh `origin`).
179    let mut min = [f64::INFINITY; 3];
180    let mut max = [f64::NEG_INFINITY; 3];
181    for w in &world {
182        for k in 0..3 {
183            min[k] = min[k].min(w[k]);
184            max[k] = max[k].max(w[k]);
185        }
186    }
187    let centre = [
188        0.5 * (min[0] + max[0]),
189        0.5 * (min[1] + max[1]),
190        0.5 * (min[2] + max[2]),
191    ];
192    let mut mesh = Mesh::new();
193    mesh.positions = world
194        .iter()
195        .flat_map(|w| {
196            [
197                (w[0] - centre[0]) as f32,
198                (w[1] - centre[1]) as f32,
199                (w[2] - centre[2]) as f32,
200            ]
201        })
202        .collect();
203    mesh.normals = if have_normals && normals.len() == mesh.positions.len() {
204        normals
205    } else {
206        Vec::new()
207    };
208    mesh.indices = indices;
209    mesh.origin = centre;
210    mesh.local_to_world = Some(l2w);
211
212    // -- Simplify.
213    let (mut out, stats) = simplify_mesh(&mesh, &SimplifyOptions::for_level(level));
214    if out.indices.is_empty() || out.positions.is_empty() {
215        return Err(SimplifySkip::EmptyResult);
216    }
217    if out.normals.len() != out.positions.len() {
218        out.normals = averaged_vertex_normals(&out.positions, &out.indices);
219    }
220
221    // -- IFC-local output: true world -> inverse placement -> file units.
222    let n_out = out.positions.len() / 3;
223    let mut local_positions: Vec<f64> = Vec::with_capacity(n_out * 3);
224    for chunk in out.positions.chunks_exact(3) {
225        let tw = [
226            chunk[0] as f64 + out.origin[0] + rtc_offset[0],
227            chunk[1] as f64 + out.origin[1] + rtc_offset[1],
228            chunk[2] as f64 + out.origin[2] + rtc_offset[2],
229        ];
230        let local = transform_point_row_major(&inv_l2w, tw);
231        local_positions.extend_from_slice(&[
232            local[0] / unit_scale,
233            local[1] / unit_scale,
234            local[2] / unit_scale,
235        ]);
236    }
237    let local_indices = out.indices.clone();
238
239    // -- Render output back in the caller's frame convention.
240    let (render_positions, render_normals, render_indices, render_origin) = if y_up {
241        let positions = out
242            .positions
243            .chunks_exact(3)
244            .flat_map(|c| {
245                let p = zup_to_yup([c[0] as f64, c[1] as f64, c[2] as f64]);
246                [p[0] as f32, p[1] as f32, p[2] as f32]
247            })
248            .collect();
249        let normals = out
250            .normals
251            .chunks_exact(3)
252            .flat_map(|c| {
253                let n = zup_to_yup([c[0] as f64, c[1] as f64, c[2] as f64]);
254                [n[0] as f32, n[1] as f32, n[2] as f32]
255            })
256            .collect();
257        (positions, normals, out.indices, zup_to_yup(out.origin))
258    } else {
259        (out.positions, out.normals, out.indices, out.origin)
260    };
261
262    Ok(SimplifiedElement {
263        render_positions,
264        render_normals,
265        render_indices,
266        render_origin,
267        local_positions,
268        local_indices,
269        tris_before: stats.tris_before,
270        tris_after: stats.tris_after,
271        cavity_components_dropped: stats.cavity_components_dropped,
272    })
273}
274