1use serde::{Deserialize, Serialize};
7
8#[cfg(target_arch = "wasm32")]
9use wasm_bindgen::prelude::*;
10
11#[cfg(target_arch = "wasm32")]
13#[wasm_bindgen]
14extern "C" {
15 #[wasm_bindgen(js_name = getIfcTimestamp)]
17 fn js_get_ifc_timestamp() -> Option<String>;
18
19 #[wasm_bindgen(js_name = getIfcGeometryBinary)]
21 fn js_get_ifc_geometry_binary() -> Option<js_sys::Uint8Array>;
22
23 #[wasm_bindgen(js_name = getIfcEntities)]
25 fn js_get_ifc_entities() -> Option<String>;
26
27 #[wasm_bindgen(js_name = clearIfcGeometryBridge)]
29 fn js_clear_ifc_geometry_bridge();
30}
31
32#[derive(Clone, Debug, Default, Serialize, Deserialize)]
34pub struct SelectionStorage {
35 pub selected_ids: Vec<u64>,
36 pub hovered_id: Option<u64>,
37}
38
39#[derive(Clone, Debug, Default, Serialize, Deserialize)]
41pub struct VisibilityStorage {
42 pub hidden: Vec<u64>,
43 pub isolated: Option<Vec<u64>>,
44}
45
46#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct CameraStorage {
49 pub azimuth: f32,
50 pub elevation: f32,
51 pub distance: f32,
52 pub target: [f32; 3],
53}
54
55impl Default for CameraStorage {
56 fn default() -> Self {
57 Self {
58 azimuth: 0.785, elevation: 0.615, distance: 10.0,
61 target: [0.0, 0.0, 0.0],
62 }
63 }
64}
65
66#[derive(Clone, Debug, Default, Serialize, Deserialize)]
68pub struct SectionStorage {
69 pub enabled: bool,
70 pub axis: String, pub position: f32, pub flipped: bool,
73}
74
75#[derive(Clone, Debug, Serialize, Deserialize)]
77pub struct FocusStorage {
78 pub entity_id: u64,
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize)]
83pub struct CameraCommandStorage {
84 pub cmd: String,
85 pub mode: Option<String>,
86}
87
88#[cfg(target_arch = "wasm32")]
94pub fn get_timestamp() -> Option<String> {
95 js_get_ifc_timestamp()
96}
97
98#[cfg(not(target_arch = "wasm32"))]
99pub fn get_timestamp() -> Option<String> {
100 None
101}
102
103#[allow(dead_code)]
105const BINARY_MAGIC: u32 = 0x49464342; #[cfg(target_arch = "wasm32")]
109fn read_f32_vec(data: &[u8], offset: &mut usize, count: usize) -> Option<Vec<f32>> {
110 let bytes_needed = count * 4;
111 if *offset + bytes_needed > data.len() {
112 return None;
113 }
114 let mut result = Vec::with_capacity(count);
115 for _ in 0..count {
116 let bytes: [u8; 4] = data[*offset..*offset + 4].try_into().ok()?;
117 result.push(f32::from_le_bytes(bytes));
118 *offset += 4;
119 }
120 Some(result)
121}
122
123#[cfg(target_arch = "wasm32")]
125fn read_u32_vec(data: &[u8], offset: &mut usize, count: usize) -> Option<Vec<u32>> {
126 let bytes_needed = count * 4;
127 if *offset + bytes_needed > data.len() {
128 return None;
129 }
130 let mut result = Vec::with_capacity(count);
131 for _ in 0..count {
132 let bytes: [u8; 4] = data[*offset..*offset + 4].try_into().ok()?;
133 result.push(u32::from_le_bytes(bytes));
134 *offset += 4;
135 }
136 Some(result)
137}
138
139#[cfg(target_arch = "wasm32")]
141fn deserialize_geometry_binary(data: &[u8]) -> Option<Vec<crate::IfcMesh>> {
142 use crate::mesh::MeshGeometry;
143 use std::sync::Arc;
144
145 if data.len() < 12 {
146 return None;
147 }
148
149 let mut offset = 0;
150
151 let magic = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?);
153 offset += 4;
154 if magic != BINARY_MAGIC {
155 web_sys::console::error_1(&format!("[Bevy] Invalid geometry magic: {:08x}", magic).into());
156 return None;
157 }
158
159 let _version = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?);
160 offset += 4;
161
162 let mesh_count = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
163 offset += 4;
164
165 let mut meshes = Vec::with_capacity(mesh_count);
166
167 for _ in 0..mesh_count {
168 if offset + 8 > data.len() {
169 break;
170 }
171
172 let entity_id = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
174 offset += 8;
175
176 let positions_len = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
178 offset += 4;
179 if offset + positions_len * 4 > data.len() {
180 break;
181 }
182 let positions = read_f32_vec(data, &mut offset, positions_len)?;
183
184 let normals_len = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
186 offset += 4;
187 if offset + normals_len * 4 > data.len() {
188 break;
189 }
190 let normals = read_f32_vec(data, &mut offset, normals_len)?;
191
192 let indices_len = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
194 offset += 4;
195 if offset + indices_len * 4 > data.len() {
196 break;
197 }
198 let indices = read_u32_vec(data, &mut offset, indices_len)?;
199
200 if offset + 16 > data.len() {
202 break;
203 }
204 let color_vec = read_f32_vec(data, &mut offset, 4)?;
205 let color: [f32; 4] = [color_vec[0], color_vec[1], color_vec[2], color_vec[3]];
206
207 if offset + 64 > data.len() {
209 break;
210 }
211 let transform_vec = read_f32_vec(data, &mut offset, 16)?;
212 let transform: [f32; 16] = transform_vec.try_into().ok()?;
213
214 if offset >= data.len() {
216 break;
217 }
218 let type_len = data[offset] as usize;
219 offset += 1;
220 if offset + type_len > data.len() {
221 break;
222 }
223 let entity_type = String::from_utf8_lossy(&data[offset..offset + type_len]).to_string();
224 offset += type_len;
225
226 if offset >= data.len() {
228 break;
229 }
230 let name_len = data[offset] as usize;
231 offset += 1;
232 let name = if name_len > 0 && offset + name_len <= data.len() {
233 let n = String::from_utf8_lossy(&data[offset..offset + name_len]).to_string();
234 offset += name_len;
235 Some(n)
236 } else {
237 None
238 };
239
240 meshes.push(crate::IfcMesh {
241 entity_id,
242 geometry: Arc::new(MeshGeometry {
243 positions,
244 normals,
245 indices,
246 }),
247 color,
248 transform,
249 entity_type,
250 name,
251 has_ifc_color: false,
252 });
253 }
254
255 Some(meshes)
256}
257
258#[cfg(target_arch = "wasm32")]
260pub fn load_geometry() -> Option<Vec<crate::IfcMesh>> {
261 let uint8_array = js_get_ifc_geometry_binary()?;
262 let data = uint8_array.to_vec();
263 web_sys::console::log_1(
264 &format!(
265 "[Bevy] Loading geometry from JS bridge: {} bytes",
266 data.len()
267 )
268 .into(),
269 );
270 let meshes = deserialize_geometry_binary(&data)?;
271 web_sys::console::log_1(&format!("[Bevy] Deserialized {} meshes", meshes.len()).into());
272 js_clear_ifc_geometry_bridge();
274 Some(meshes)
275}
276
277#[cfg(not(target_arch = "wasm32"))]
278pub fn load_geometry() -> Option<Vec<crate::IfcMesh>> {
279 None
280}
281
282#[cfg(target_arch = "wasm32")]
284pub fn load_entities() -> Option<Vec<crate::EntityInfo>> {
285 let json = js_get_ifc_entities()?;
286 serde_json::from_str(&json).ok()
287}
288
289#[cfg(not(target_arch = "wasm32"))]
290pub fn load_entities() -> Option<Vec<crate::EntityInfo>> {
291 None
292}
293
294#[cfg(target_arch = "wasm32")]
296pub fn load_selection() -> Option<SelectionStorage> {
297 let storage = web_sys::window()?.local_storage().ok()??;
298 let json = storage.get_item("ifc_lite_selection").ok()??;
299 serde_json::from_str(&json).ok()
300}
301
302#[cfg(not(target_arch = "wasm32"))]
303pub fn load_selection() -> Option<SelectionStorage> {
304 None
305}
306
307#[cfg(target_arch = "wasm32")]
309pub fn save_selection(selection: &SelectionStorage) {
310 if let Some(window) = web_sys::window() {
311 if let Ok(Some(storage)) = window.local_storage() {
312 if let Ok(json) = serde_json::to_string(selection) {
313 let _ = storage.set_item("ifc_lite_selection", &json);
314 let _ = storage.set_item("ifc_lite_selection_source", "bevy");
316 }
317 }
318 }
319}
320
321#[cfg(not(target_arch = "wasm32"))]
322pub fn save_selection(_selection: &SelectionStorage) {}
323
324#[cfg(target_arch = "wasm32")]
326pub fn get_selection_source() -> Option<String> {
327 let storage = web_sys::window()?.local_storage().ok()??;
328 storage.get_item("ifc_lite_selection_source").ok()?
329}
330
331#[cfg(not(target_arch = "wasm32"))]
332pub fn get_selection_source() -> Option<String> {
333 None
334}
335
336#[cfg(target_arch = "wasm32")]
338pub fn load_visibility() -> Option<VisibilityStorage> {
339 let storage = web_sys::window()?.local_storage().ok()??;
340 let json = storage.get_item("ifc_lite_visibility").ok()??;
341 serde_json::from_str(&json).ok()
342}
343
344#[cfg(not(target_arch = "wasm32"))]
345pub fn load_visibility() -> Option<VisibilityStorage> {
346 None
347}
348
349#[cfg(target_arch = "wasm32")]
351pub fn load_camera() -> Option<CameraStorage> {
352 let storage = web_sys::window()?.local_storage().ok()??;
353 let json = storage.get_item("ifc_lite_camera").ok()??;
354 serde_json::from_str(&json).ok()
355}
356
357#[cfg(not(target_arch = "wasm32"))]
358pub fn load_camera() -> Option<CameraStorage> {
359 None
360}
361
362#[cfg(target_arch = "wasm32")]
364pub fn save_camera(camera: &CameraStorage) {
365 if let Some(window) = web_sys::window() {
366 if let Ok(Some(storage)) = window.local_storage() {
367 if let Ok(json) = serde_json::to_string(camera) {
368 let _ = storage.set_item("ifc_lite_camera", &json);
369 }
370 }
371 }
372}
373
374#[cfg(not(target_arch = "wasm32"))]
375pub fn save_camera(_camera: &CameraStorage) {}
376
377#[cfg(target_arch = "wasm32")]
379pub fn load_section() -> Option<SectionStorage> {
380 let storage = web_sys::window()?.local_storage().ok()??;
381 let json = storage.get_item("ifc_lite_section").ok()??;
382 serde_json::from_str(&json).ok()
383}
384
385#[cfg(not(target_arch = "wasm32"))]
386pub fn load_section() -> Option<SectionStorage> {
387 None
388}
389
390#[cfg(target_arch = "wasm32")]
392pub fn load_focus() -> Option<FocusStorage> {
393 let storage = web_sys::window()?.local_storage().ok()??;
394 let json = storage.get_item("ifc_lite_focus").ok()??;
395 serde_json::from_str(&json).ok()
396}
397
398#[cfg(not(target_arch = "wasm32"))]
399pub fn load_focus() -> Option<FocusStorage> {
400 None
401}
402
403#[cfg(target_arch = "wasm32")]
405pub fn clear_focus() {
406 if let Some(window) = web_sys::window() {
407 if let Ok(Some(storage)) = window.local_storage() {
408 let _ = storage.remove_item("ifc_lite_focus");
409 }
410 }
411}
412
413#[cfg(not(target_arch = "wasm32"))]
414pub fn clear_focus() {}
415
416#[cfg(target_arch = "wasm32")]
418pub fn load_camera_cmd() -> Option<CameraCommandStorage> {
419 let storage = web_sys::window()?.local_storage().ok()??;
420 let json = storage.get_item("ifc_lite_camera_cmd").ok()??;
421 serde_json::from_str(&json).ok()
422}
423
424#[cfg(not(target_arch = "wasm32"))]
425pub fn load_camera_cmd() -> Option<CameraCommandStorage> {
426 None
427}
428
429#[cfg(target_arch = "wasm32")]
431pub fn clear_camera_cmd() {
432 if let Some(window) = web_sys::window() {
433 if let Ok(Some(storage)) = window.local_storage() {
434 let _ = storage.remove_item("ifc_lite_camera_cmd");
435 }
436 }
437}
438
439#[cfg(not(target_arch = "wasm32"))]
440pub fn clear_camera_cmd() {}
441
442#[cfg(target_arch = "wasm32")]
444pub fn load_palette() -> Option<String> {
445 let storage = web_sys::window()?.local_storage().ok()??;
446 storage.get_item("ifc_lite_palette").ok()?
447}
448
449#[cfg(not(target_arch = "wasm32"))]
450pub fn load_palette() -> Option<String> {
451 None
452}
453
454#[cfg(target_arch = "wasm32")]
456pub fn clear_palette() {
457 if let Some(window) = web_sys::window() {
458 if let Ok(Some(storage)) = window.local_storage() {
459 let _ = storage.remove_item("ifc_lite_palette");
460 }
461 }
462}
463
464#[cfg(not(target_arch = "wasm32"))]
465pub fn clear_palette() {}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct MeasurePointStorage {
470 pub x: f32,
471 pub y: f32,
472 pub z: f32,
473}
474
475#[cfg(target_arch = "wasm32")]
477pub fn save_measure_point(point: &MeasurePointStorage) {
478 if let Some(window) = web_sys::window() {
479 if let Ok(Some(storage)) = window.local_storage() {
480 if let Ok(json) = serde_json::to_string(point) {
481 let _ = storage.set_item("ifc_lite_measure_point", &json);
482 }
483 }
484 }
485}
486
487#[cfg(not(target_arch = "wasm32"))]
488pub fn save_measure_point(_point: &MeasurePointStorage) {}
489
490#[cfg(target_arch = "wasm32")]
492pub fn load_active_tool() -> Option<String> {
493 let storage = web_sys::window()?.local_storage().ok()??;
494 storage.get_item("ifc_lite_active_tool").ok()?
495}
496
497#[cfg(not(target_arch = "wasm32"))]
498pub fn load_active_tool() -> Option<String> {
499 None
500}
501
502#[cfg(target_arch = "wasm32")]
504pub fn load_lighting_cmd() -> Option<String> {
505 let storage = web_sys::window()?.local_storage().ok()??;
506 storage.get_item("ifc_lite_lighting_cmd").ok()?
507}
508
509#[cfg(not(target_arch = "wasm32"))]
510pub fn load_lighting_cmd() -> Option<String> {
511 None
512}
513
514#[cfg(target_arch = "wasm32")]
516pub fn clear_lighting_cmd() {
517 if let Some(window) = web_sys::window() {
518 if let Ok(Some(storage)) = window.local_storage() {
519 let _ = storage.remove_item("ifc_lite_lighting_cmd");
520 }
521 }
522}
523
524#[cfg(not(target_arch = "wasm32"))]
525pub fn clear_lighting_cmd() {}