1#![allow(unexpected_cfgs)]
10
11pub mod camera;
12pub mod loader;
13pub mod mesh;
14#[cfg(feature = "photometric")]
15pub mod photometric;
16pub mod picking;
17pub mod section;
18pub mod storage;
19
20#[cfg(feature = "bevy-ui")]
21pub mod ui;
22
23#[cfg(any(target_os = "ios", target_os = "macos"))]
24pub mod native_view;
25
26#[cfg(any(target_os = "ios", target_os = "macos"))]
27pub mod ffi;
28
29use bevy::prelude::*;
30use rustc_hash::FxHashSet;
31use serde::{Deserialize, Serialize};
32use std::sync::atomic::{AtomicBool, Ordering};
33use std::sync::Mutex;
34
35static DEBUG_MODE: AtomicBool = AtomicBool::new(false);
37
38static PENDING_MESHES: Mutex<Option<Vec<IfcMesh>>> = Mutex::new(None);
41
42pub fn set_pending_meshes(meshes: Vec<IfcMesh>) {
45 let count = meshes.len();
46 let mut guard = PENDING_MESHES.lock().unwrap();
47 *guard = Some(meshes);
48 log(&format!("[Bevy] Pending meshes set: {} meshes", count));
49}
50
51pub fn take_pending_meshes() -> Option<Vec<IfcMesh>> {
53 let mut guard = PENDING_MESHES.lock().unwrap();
54 guard.take()
55}
56
57pub fn has_pending_meshes() -> bool {
59 let guard = PENDING_MESHES.lock().unwrap();
60 guard.is_some()
61}
62
63pub fn is_debug() -> bool {
65 DEBUG_MODE.load(Ordering::Relaxed)
66}
67
68#[cfg(target_arch = "wasm32")]
70fn init_debug_from_url() {
71 if let Some(window) = web_sys::window() {
72 if let Ok(search) = window.location().search() {
73 let search_str: &str = &search;
74 if search_str.contains("debug=1") || search_str.contains("debug=true") {
75 DEBUG_MODE.store(true, Ordering::Relaxed);
76 web_sys::console::log_1(&"[Bevy] Debug mode enabled".into());
77 }
78 }
79 }
80}
81
82#[cfg(not(target_arch = "wasm32"))]
83#[allow(dead_code)]
84fn init_debug_from_url() {
85 if std::env::var("DEBUG").is_ok() {
87 DEBUG_MODE.store(true, Ordering::Relaxed);
88 }
89}
90
91pub use camera::{CameraController, CameraMode, CameraPlugin};
93pub use loader::{LoadIfcContentEvent, LoadIfcFileEvent, LoaderPlugin, OpenFileDialogRequest};
94pub use mesh::{AutoFitState, IfcEntity, IfcMesh, IfcMeshSerialized, MeshGeometry, MeshPlugin};
95pub use picking::{PickingPlugin, SelectionState};
96pub use section::{SectionPlane, SectionPlanePlugin};
97pub use storage::*;
98
99#[cfg(feature = "bevy-ui")]
100pub use ui::{IfcUiPlugin, UiState};
101
102#[cfg(any(target_os = "ios", target_os = "macos"))]
103pub use native_view::{AppView, AppViewPlugin, AppViews};
104
105pub struct IfcViewerPlugin;
107
108impl Plugin for IfcViewerPlugin {
109 fn build(&self, app: &mut App) {
110 app.init_resource::<IfcSceneData>()
111 .init_resource::<ViewerSettings>()
112 .init_resource::<IfcTimestamp>()
113 .add_plugins((
114 CameraPlugin,
115 MeshPlugin,
116 PickingPlugin,
117 SectionPlanePlugin,
118 LoaderPlugin,
119 ))
120 .add_systems(Update, (poll_scene_changes, poll_selection_from_storage));
121
122 #[cfg(feature = "bevy-ui")]
124 app.add_plugins(IfcUiPlugin);
125
126 #[cfg(feature = "photometric")]
128 app.add_plugins(photometric::PhotometricLightingPlugin);
129 }
130}
131
132#[derive(Resource, Default)]
134pub struct IfcSceneData {
135 pub meshes: Vec<IfcMesh>,
137 pub entities: Vec<EntityInfo>,
139 pub bounds: Option<SceneBounds>,
141 pub timestamp: u64,
143 pub dirty: bool,
145}
146
147#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct EntityInfo {
150 pub id: u64,
151 pub entity_type: String,
152 pub name: Option<String>,
153 pub storey: Option<String>,
154 pub storey_elevation: Option<f32>,
155}
156
157#[derive(Clone, Debug, Default)]
159pub struct SceneBounds {
160 pub min: Vec3,
161 pub max: Vec3,
162}
163
164impl SceneBounds {
165 pub fn center(&self) -> Vec3 {
166 (self.min + self.max) * 0.5
167 }
168
169 pub fn size(&self) -> Vec3 {
170 self.max - self.min
171 }
172
173 pub fn diagonal(&self) -> f32 {
174 self.size().length()
175 }
176}
177
178#[derive(Resource)]
180pub struct ViewerSettings {
181 pub theme: Theme,
183 pub show_grid: bool,
185 pub show_axes: bool,
187 pub hidden_entities: FxHashSet<u64>,
189 pub isolated_entities: Option<FxHashSet<u64>>,
191 pub storey_filter: Option<String>,
193}
194
195impl Default for ViewerSettings {
196 fn default() -> Self {
197 Self {
198 theme: Theme::Dark,
199 show_grid: true,
200 show_axes: true,
201 hidden_entities: FxHashSet::default(),
202 isolated_entities: None,
203 storey_filter: None,
204 }
205 }
206}
207
208#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
210pub enum Theme {
211 Light,
212 #[default]
213 Dark,
214}
215
216impl Theme {
217 pub fn background_color(&self) -> Color {
218 match self {
219 Theme::Light => Color::srgb(0.95, 0.95, 0.95),
220 Theme::Dark => Color::srgb(0.12, 0.12, 0.12),
221 }
222 }
223
224 pub fn grid_color(&self) -> Color {
225 match self {
226 Theme::Light => Color::srgba(0.5, 0.5, 0.5, 0.3),
227 Theme::Dark => Color::srgba(0.4, 0.4, 0.4, 0.3),
228 }
229 }
230}
231
232#[derive(Resource, Default)]
234pub struct IfcTimestamp(pub String);
235
236#[allow(unused_variables, unused_mut)]
239pub fn poll_scene_changes(
240 mut scene_data: ResMut<IfcSceneData>,
241 mut settings: ResMut<ViewerSettings>,
242 mut last_timestamp: ResMut<IfcTimestamp>,
243 mut auto_fit: ResMut<mesh::AutoFitState>,
244) {
245 if let Some(meshes) = take_pending_meshes() {
247 log_info(&format!(
248 "[Bevy] Direct mesh transfer: {} meshes (no deserialization!)",
249 meshes.len()
250 ));
251
252 scene_data.entities = meshes
254 .iter()
255 .map(|m| EntityInfo {
256 id: m.entity_id,
257 entity_type: m.entity_type.clone(),
258 name: m.name.clone(),
259 storey: None,
260 storey_elevation: None,
261 })
262 .collect();
263
264 scene_data.meshes = meshes;
265 scene_data.dirty = true;
266 auto_fit.has_fit = false;
267 } else {
268 #[cfg(target_arch = "wasm32")]
270 {
271 if let Some(new_timestamp) = storage::get_timestamp() {
272 if new_timestamp != last_timestamp.0 {
273 log(&format!(
274 "[Bevy] Timestamp changed: {} -> {}",
275 last_timestamp.0, new_timestamp
276 ));
277
278 if let Some(geometry) = storage::load_geometry() {
280 log(&format!(
281 "[Bevy] Loaded {} meshes from JS bridge",
282 geometry.len()
283 ));
284
285 scene_data.entities = geometry
287 .iter()
288 .map(|m| EntityInfo {
289 id: m.entity_id,
290 entity_type: m.entity_type.clone(),
291 name: m.name.clone(),
292 storey: None,
293 storey_elevation: None,
294 })
295 .collect();
296
297 scene_data.meshes = geometry;
298 scene_data.dirty = true;
299 auto_fit.has_fit = false;
300 }
301
302 if let Some(selection) = storage::load_selection() {
304 }
306
307 if let Some(visibility) = storage::load_visibility() {
309 settings.hidden_entities = visibility.hidden.into_iter().collect();
310 settings.isolated_entities =
311 visibility.isolated.map(|v| v.into_iter().collect());
312 }
313
314 last_timestamp.0 = new_timestamp;
315 }
316 }
317 }
318 }
319}
320
321#[allow(unused_variables, unused_mut)]
324pub fn poll_selection_from_storage(mut selection: ResMut<picking::SelectionState>) {
325 #[cfg(target_arch = "wasm32")]
326 {
327 if let Some(stored_selection) = storage::load_selection() {
329 if let Some(source) = storage::get_selection_source() {
332 if source == "bevy" {
333 return;
334 }
335 }
336
337 let new_selection: FxHashSet<u64> = stored_selection.selected_ids.into_iter().collect();
339
340 if selection.selected != new_selection {
342 selection.selected = new_selection;
343 }
345 }
346 }
347}
348
349#[cfg(target_arch = "wasm32")]
351pub fn log(msg: &str) {
352 if is_debug() {
353 web_sys::console::log_1(&msg.into());
354 }
355}
356
357#[cfg(not(target_arch = "wasm32"))]
358pub fn log(msg: &str) {
359 if is_debug() {
360 println!("{}", msg);
361 }
362}
363
364#[cfg(target_arch = "wasm32")]
366pub fn log_info(msg: &str) {
367 web_sys::console::info_1(&msg.into());
368}
369
370#[cfg(not(target_arch = "wasm32"))]
371pub fn log_info(msg: &str) {
372 println!("{}", msg);
373}
374
375#[cfg(target_arch = "wasm32")]
382#[wasm_bindgen::prelude::wasm_bindgen]
383pub fn run_on_canvas(canvas_selector: &str) {
384 console_error_panic_hook::set_once();
385 init_debug_from_url();
386 log_info(&format!(
387 "[Bevy] Starting unified viewer on canvas: {}",
388 canvas_selector
389 ));
390
391 let scene_data = IfcSceneData::default();
393
394 let mut app = App::new();
395
396 app.insert_resource(scene_data);
398 app.insert_resource(ViewerSettings::default());
399 app.insert_resource(IfcTimestamp::default());
400
401 app.add_plugins(DefaultPlugins.set(WindowPlugin {
403 primary_window: Some(Window {
404 title: "BIMIFC Viewer".to_string(),
405 canvas: Some(canvas_selector.to_string()),
406 fit_canvas_to_parent: true,
407 prevent_default_event_handling: false,
408 ..default()
409 }),
410 ..default()
411 }));
412
413 app.add_plugins(IfcViewerPlugin);
414 app.run();
415}
416
417#[cfg(not(target_arch = "wasm32"))]
419pub fn run_on_canvas(_canvas_selector: &str) {
420 run_native();
421}
422
423#[cfg(not(target_arch = "wasm32"))]
425pub fn run_native() {
426 App::new()
427 .add_plugins(DefaultPlugins.set(WindowPlugin {
428 primary_window: Some(Window {
429 title: "BIMIFC Viewer".to_string(),
430 resolution: (1280u32, 720u32).into(),
431 ..default()
432 }),
433 ..default()
434 }))
435 .insert_resource(ClearColor(Color::srgb(0.1, 0.1, 0.15)))
437 .add_plugins(IfcViewerPlugin)
438 .run();
439}
440
441#[cfg(target_arch = "wasm32")]
442pub fn run_native() {
443 run_on_canvas("#bevy-canvas");
444}
445
446#[cfg(target_arch = "wasm32")]
448#[wasm_bindgen::prelude::wasm_bindgen]
449pub fn wasm_start() {
450 log("[Bevy] wasm_start called");
451 run_native();
452}
453
454#[cfg(target_arch = "wasm32")]
457pub fn run_with_data(canvas_selector: &str, scene_data: IfcSceneData) {
458 console_error_panic_hook::set_once();
459 init_debug_from_url();
460 log_info(&format!(
461 "[Bevy] Starting with data: {} meshes, {} entities",
462 scene_data.meshes.len(),
463 scene_data.entities.len()
464 ));
465
466 let mut app = App::new();
467
468 app.insert_resource(scene_data);
470 app.insert_resource(ViewerSettings::default());
471 app.insert_resource(IfcTimestamp::default());
472
473 app.add_plugins(DefaultPlugins.set(WindowPlugin {
475 primary_window: Some(Window {
476 title: "BIMIFC Viewer".to_string(),
477 canvas: Some(canvas_selector.to_string()),
478 fit_canvas_to_parent: true,
479 prevent_default_event_handling: false,
480 ..default()
481 }),
482 ..default()
483 }));
484
485 app.add_plugins(IfcViewerPlugin);
486 app.run();
487}
488
489#[cfg(not(target_arch = "wasm32"))]
490pub fn run_with_data(_canvas_selector: &str, _scene_data: IfcSceneData) {
491 run_native();
492}