Skip to main content

ifc_lite_wasm/api/
export_glb.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//! WASM API: export_glb — IFC render geometry → binary glTF (GLB) bytes.
6
7use super::IfcAPI;
8use wasm_bindgen::prelude::*;
9
10/// Map the optional KML altitude-mode string from the JS boundary to the
11/// exporter enum. `None` (or any unrecognised value) ⇒ `ClampToGround` so the
12/// safe, non-floating default (#1427) is preserved and existing callers that
13/// omit the argument are unchanged. The UI exposes only `"clampToGround"`
14/// ("Rest on ground") and `"absolute"` ("True elevation (MSL)"); the literal
15/// `"relativeToGround"` is accepted for completeness.
16fn kmz_altitude_mode(mode: Option<String>) -> ifc_lite_export::AltitudeMode {
17    use ifc_lite_export::AltitudeMode;
18    match mode.as_deref() {
19        Some("absolute") => AltitudeMode::Absolute,
20        Some("relativeToGround") => AltitudeMode::RelativeToGround,
21        _ => AltitudeMode::ClampToGround,
22    }
23}
24
25#[wasm_bindgen]
26impl IfcAPI {
27    /// Export the render geometry in `content` as a binary **GLB** (`Uint8Array`).
28    ///
29    /// `hidden` / `isolated` are express-id visibility filters; `hidden_types_csv` is a
30    /// comma-separated list of IFC type names whose class toggle is off (e.g.
31    /// `"IfcOpeningElement,IfcSpace"`). `include_metadata` attaches counts + per-node
32    /// `expressId`. Per-mesh RTC origin rides the node translation (precision-safe).
33    /// `lit` emits standard PBR materials that shade from normals; omitted or
34    /// `true` ⇒ lit (the default), `false` ⇒ flat `KHR_materials_unlit` (the
35    /// historical look — #1321). Optional at the boundary so older 5-arg callers
36    /// keep lit-by-default behaviour.
37    /// `emissive` self-illuminates each material at its base colour (core glTF
38    /// `emissiveFactor`) so renderers without ambient/IBL — Google Earth — don't
39    /// render the model near-black (#1427); omitted or `false` ⇒ off.
40    ///
41    /// Fails CLOSED: when the visible mesh set is empty this throws an `Error`
42    /// whose message starts with `NO_RENDER_GEOMETRY`, instead of returning a
43    /// structurally valid but empty GLB. #1438 put that guard only in the TS
44    /// CLI/MCP wrappers; making the boundary itself refuse means SDK/viewer/
45    /// direct callers inherit it too (the TS guards stay as defense-in-depth).
46    #[wasm_bindgen(js_name = exportGlb)]
47    #[allow(clippy::too_many_arguments)]
48    pub fn export_glb(
49        &self,
50        content: &[u8],
51        include_metadata: bool,
52        hidden: &[u32],
53        isolated: &[u32],
54        hidden_types_csv: String,
55        lit: Option<bool>,
56        emissive: Option<bool>,
57    ) -> Result<Vec<u8>, JsValue> {
58        let hidden_types = hidden_types_csv
59            .split(',')
60            .map(|s| s.trim().to_string())
61            .filter(|s| !s.is_empty())
62            .collect();
63        let opts = ifc_lite_export::GltfOptions::default()
64            .with_include_metadata(include_metadata)
65            .with_hidden(hidden.to_vec())
66            .with_isolated(isolated.to_vec())
67            .with_hidden_types(hidden_types)
68            .with_lit(lit.unwrap_or(true))
69            .with_emissive(emissive.unwrap_or(false))
70            // Federation (modelId stamping) is a server-side concern; the viewer's
71            // wasm export path is single-model. Add a parameter here if/when the
72            // browser needs to federate.
73            .with_model_id(None)
74            // The viewer loads the GLB directly; quantization is a server/export-pipeline
75            // concern (KHR_mesh_quantization needs loader support the viewer doesn't wire).
76            .with_quantize(false)
77            // Stated rather than inherited from `setTessellationQuality`, so this
78            // export keeps emitting exactly what it emitted before. Whether an
79            // export should follow the density the viewer is displaying at is a
80            // separate question from whether a caller can name one.
81            .with_tessellation_quality(ifc_lite_export::TessellationQuality::Medium);
82        ifc_lite_export::try_export_glb(content, &opts)
83            .map_err(|e| JsValue::from(js_sys::Error::new(&e.to_string())))
84    }
85
86    /// Assemble a **GLB** from already-produced meshes (the viewer's `MeshData`, flattened)
87    /// — no re-meshing. Per mesh `i`: `vertex_counts[i]` verts + `index_counts[i]` indices
88    /// taken in order from the concatenated `positions`/`normals`/`indices`; `colors` is
89    /// RGBA per mesh, `origins` xyz per mesh, `express_ids` labels each mesh (indices are
90    /// per-mesh local). The caller passes exactly the meshes it wants emitted.
91    ///
92    /// Fails CLOSED: if the declared vertex/index counts run past the flattened
93    /// `positions` / `indices`, there are fewer `index_counts` than meshes, or `normals`
94    /// is empty or too short to cover every vertex, this throws an `Error` whose message
95    /// starts with `MALFORMED_MESH_INPUT` — instead of silently emitting a GLB with those
96    /// meshes dropped. (The viewer always passes fully-backed, normal-covered arrays, so
97    /// this only fires on a caller bug.)
98    #[wasm_bindgen(js_name = exportGlbFromMeshes)]
99    #[allow(clippy::too_many_arguments)]
100    pub fn export_glb_from_meshes(
101        &self,
102        positions: &[f32],
103        normals: &[f32],
104        indices: &[u32],
105        vertex_counts: &[u32],
106        index_counts: &[u32],
107        colors: &[f32],
108        origins: &[f64],
109        express_ids: &[u32],
110        include_metadata: bool,
111        lit: Option<bool>,
112        emissive: Option<bool>,
113    ) -> Result<Vec<u8>, JsValue> {
114        ifc_lite_export::try_export_glb_from_meshes(
115            positions,
116            normals,
117            indices,
118            vertex_counts,
119            index_counts,
120            colors,
121            origins,
122            express_ids,
123            include_metadata,
124            lit.unwrap_or(true),
125            emissive.unwrap_or(false),
126        )
127        .map(|(glb, _)| glb)
128        .map_err(|e| JsValue::from(js_sys::Error::new(&e.to_string())))
129    }
130
131    /// Package an already-produced **GLB** + georeference into a **KMZ** (`Uint8Array`)
132    /// for Google Earth: a ZIP of `doc.kml` (a `<Model>` placed at `latitude`/`longitude`/
133    /// `altitude`) + `model.glb`. `x_axis_abscissa`/`x_axis_ordinate` are the
134    /// `IfcMapConversion` grid-north components; pass both as `undefined` for heading 0.
135    ///
136    /// `altitude_mode` selects the KML vertical placement: `"clampToGround"`
137    /// (the default when omitted) rests the model on the terrain, ignoring
138    /// `altitude`; `"absolute"` places the origin at `altitude` metres MSL.
139    /// Google Earth's terrain already encodes the site elevation, so clamping
140    /// keeps a wrong/zero/double-counted OrthogonalHeight from floating the
141    /// model into the sky (#1427); absolute is offered for models whose
142    /// OrthogonalHeight is a true MSL elevation the user wants honoured.
143    #[wasm_bindgen(js_name = exportKmz)]
144    #[allow(clippy::too_many_arguments)]
145    pub fn export_kmz(
146        &self,
147        glb: &[u8],
148        latitude: f64,
149        longitude: f64,
150        altitude: f64,
151        x_axis_abscissa: Option<f64>,
152        x_axis_ordinate: Option<f64>,
153        name: String,
154        altitude_mode: Option<String>,
155    ) -> Vec<u8> {
156        let opts = ifc_lite_export::KmzOptions {
157            latitude,
158            longitude,
159            altitude,
160            altitude_mode: kmz_altitude_mode(altitude_mode),
161            x_axis_abscissa,
162            x_axis_ordinate,
163            name: if name.is_empty() { None } else { Some(name) },
164        };
165        ifc_lite_export::export_kmz(glb, &opts)
166    }
167
168    /// Build a Google-Earth-ready **KMZ** (`Uint8Array`) straight from the viewer's
169    /// already-produced meshes — the working path (#1427). The model is embedded as
170    /// **COLLADA** (`model.dae`), the only `<Model>` format Google Earth loads (a GLB
171    /// raises "Unsupported element: Model"), with emission-lit double-sided materials
172    /// placement. Mesh arrays match `exportGlbFromMeshes`;
173    /// `latitude`/`longitude`/`altitude` + `x_axis_abscissa`/`x_axis_ordinate`
174    /// (grid-north, `undefined` ⇒ heading 0) place + orient the model.
175    /// `altitude_mode` (`"clampToGround"` default ⇒ rest on terrain, ignoring
176    /// `altitude`; `"absolute"` ⇒ place at `altitude` metres MSL) selects the
177    /// KML vertical placement (#1427).
178    #[wasm_bindgen(js_name = exportKmzFromMeshes)]
179    #[allow(clippy::too_many_arguments)]
180    pub fn export_kmz_from_meshes(
181        &self,
182        positions: &[f32],
183        normals: &[f32],
184        indices: &[u32],
185        vertex_counts: &[u32],
186        index_counts: &[u32],
187        colors: &[f32],
188        origins: &[f64],
189        latitude: f64,
190        longitude: f64,
191        altitude: f64,
192        x_axis_abscissa: Option<f64>,
193        x_axis_ordinate: Option<f64>,
194        name: String,
195        altitude_mode: Option<String>,
196    ) -> Vec<u8> {
197        let opts = ifc_lite_export::KmzOptions {
198            latitude,
199            longitude,
200            altitude,
201            altitude_mode: kmz_altitude_mode(altitude_mode),
202            x_axis_abscissa,
203            x_axis_ordinate,
204            name: if name.is_empty() { None } else { Some(name) },
205        };
206        ifc_lite_export::export_kmz_collada_from_meshes(
207            positions,
208            normals,
209            indices,
210            vertex_counts,
211            index_counts,
212            colors,
213            origins,
214            &opts,
215        )
216    }
217}