Skip to main content

bimifc_bevy/
storage.rs

1//! Storage types and localStorage bridge
2//!
3//! In unified mode (bevy-ui): No external JS bridge needed, files loaded directly
4//! In external-ui mode: Uses localStorage/JS bridge to communicate with Yew
5
6use serde::{Deserialize, Serialize};
7
8#[cfg(target_arch = "wasm32")]
9use wasm_bindgen::prelude::*;
10
11// JavaScript FFI to get geometry from JS bridge (set by Yew)
12#[cfg(target_arch = "wasm32")]
13#[wasm_bindgen]
14extern "C" {
15    /// Get timestamp from JS bridge
16    #[wasm_bindgen(js_name = getIfcTimestamp)]
17    fn js_get_ifc_timestamp() -> Option<String>;
18
19    /// Get geometry binary from JS bridge
20    #[wasm_bindgen(js_name = getIfcGeometryBinary)]
21    fn js_get_ifc_geometry_binary() -> Option<js_sys::Uint8Array>;
22
23    /// Get entities JSON from JS bridge
24    #[wasm_bindgen(js_name = getIfcEntities)]
25    fn js_get_ifc_entities() -> Option<String>;
26
27    /// Clear geometry from JS bridge to free memory
28    #[wasm_bindgen(js_name = clearIfcGeometryBridge)]
29    fn js_clear_ifc_geometry_bridge();
30}
31
32/// Selection state for storage
33#[derive(Clone, Debug, Default, Serialize, Deserialize)]
34pub struct SelectionStorage {
35    pub selected_ids: Vec<u64>,
36    pub hovered_id: Option<u64>,
37}
38
39/// Visibility state for storage
40#[derive(Clone, Debug, Default, Serialize, Deserialize)]
41pub struct VisibilityStorage {
42    pub hidden: Vec<u64>,
43    pub isolated: Option<Vec<u64>>,
44}
45
46/// Camera state for storage
47#[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,   // 45 degrees
59            elevation: 0.615, // ~35 degrees (isometric)
60            distance: 10.0,
61            target: [0.0, 0.0, 0.0],
62        }
63    }
64}
65
66/// Section plane state for storage
67#[derive(Clone, Debug, Default, Serialize, Deserialize)]
68pub struct SectionStorage {
69    pub enabled: bool,
70    pub axis: String,  // "x", "y", or "z"
71    pub position: f32, // 0.0 to 1.0
72    pub flipped: bool,
73}
74
75/// Focus command for zooming to entity
76#[derive(Clone, Debug, Serialize, Deserialize)]
77pub struct FocusStorage {
78    pub entity_id: u64,
79}
80
81/// Camera command from UI
82#[derive(Clone, Debug, Serialize, Deserialize)]
83pub struct CameraCommandStorage {
84    pub cmd: String,
85    pub mode: Option<String>,
86}
87
88// ============================================================================
89// JS Bridge functions - used in unified Yew+Bevy mode
90// ============================================================================
91
92/// Get timestamp from JS bridge
93#[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/// Binary format magic number (must match bridge.rs)
104#[allow(dead_code)]
105const BINARY_MAGIC: u32 = 0x49464342; // "IFCB"
106
107/// Read f32 values from unaligned byte slice
108#[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/// Read u32 values from unaligned byte slice
124#[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/// Deserialize geometry from binary format
140#[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    // Read header
152    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        // entity_id
173        let entity_id = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
174        offset += 8;
175
176        // positions
177        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        // normals
185        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        // indices
193        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        // color (4 floats)
201        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        // transform (16 floats)
208        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        // entity_type
215        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        // name
227        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/// Load geometry from JS bridge
259#[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    // Clear the JS bridge to free memory
273    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/// Load entities from JS bridge
283#[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/// Load selection from localStorage
295#[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/// Save selection to localStorage
308#[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                // Mark source as "bevy" so Yew knows to pick up this change
315                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/// Get selection source from localStorage
325#[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/// Load visibility from localStorage
337#[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/// Load camera from localStorage
350#[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/// Save camera to localStorage
363#[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/// Load section plane from localStorage
378#[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/// Load focus command from localStorage
391#[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/// Clear focus command
404#[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/// Load camera command from localStorage
417#[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/// Clear camera command
430#[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/// Load palette from localStorage
443#[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/// Clear palette
455#[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/// Measurement point storage
468#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct MeasurePointStorage {
470    pub x: f32,
471    pub y: f32,
472    pub z: f32,
473}
474
475/// Save a measurement point to localStorage (Bevy → Leptos)
476#[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/// Load active tool mode from localStorage
491#[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/// Load lighting toggle command from localStorage
503#[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/// Clear lighting command
515#[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() {}