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