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}