1use crate::mesh::IfcMesh;
4use crate::{EntityInfo, IfcSceneData};
5use bevy::prelude::*;
6#[cfg(all(not(target_arch = "wasm32"), not(target_os = "ios"),))]
7use bevy::tasks::IoTaskPool;
8use bevy::tasks::Task;
9use bimifc_geometry::GeometryRouter;
10use bimifc_model::{AttributeValue, EntityId, EntityResolver, IfcModel, IfcType};
11use bimifc_parser::{EntityScanner, ParsedModel};
12use rustc_hash::FxHashMap;
13use std::path::PathBuf;
14use std::sync::Arc;
15
16#[cfg(target_arch = "wasm32")]
17use wasm_bindgen::prelude::*;
18#[cfg(target_arch = "wasm32")]
19use wasm_bindgen::JsCast;
20
21pub struct LoaderPlugin;
23
24impl Plugin for LoaderPlugin {
25 fn build(&self, app: &mut App) {
26 app.add_message::<LoadIfcFileEvent>()
27 .add_message::<LoadIfcContentEvent>()
28 .add_message::<IfcFileLoadedEvent>()
29 .add_message::<OpenFileDialogRequest>()
30 .init_resource::<FileDialogState>()
31 .add_systems(
32 Update,
33 (
34 handle_open_dialog_request,
35 poll_file_dialog,
36 poll_wasm_file_input,
37 handle_load_file_event,
38 handle_load_content_event,
39 handle_file_drop,
40 ),
41 );
42
43 #[cfg(target_arch = "wasm32")]
45 {
46 setup_wasm_file_input();
47 }
48 }
49}
50
51fn poll_wasm_file_input(mut content_events: MessageWriter<LoadIfcContentEvent>) {
53 if let Some((file_name, content)) = poll_pending_file() {
54 content_events.write(LoadIfcContentEvent { file_name, content });
55 }
56}
57
58#[derive(Message)]
60pub struct OpenFileDialogRequest;
61
62#[derive(Resource, Default)]
64pub struct FileDialogState {
65 task: Option<Task<Option<PathBuf>>>,
66}
67
68#[derive(Message)]
70pub struct LoadIfcFileEvent {
71 pub path: std::path::PathBuf,
72}
73
74#[derive(Message)]
76pub struct LoadIfcContentEvent {
77 pub file_name: String,
78 pub content: String,
79}
80
81#[derive(Message)]
83pub struct IfcFileLoadedEvent {
84 pub path: PathBuf,
85 pub entity_count: usize,
86 pub mesh_count: usize,
87}
88
89#[cfg(all(not(target_arch = "wasm32"), not(target_os = "ios"),))]
91fn handle_open_dialog_request(
92 mut requests: MessageReader<OpenFileDialogRequest>,
93 mut state: ResMut<FileDialogState>,
94) {
95 for _ in requests.read() {
96 if state.task.is_some() {
98 crate::log("[Loader] File dialog already open");
99 continue;
100 }
101
102 crate::log_info("[Loader] Opening file dialog...");
103
104 let task_pool = IoTaskPool::get();
105 let task = task_pool.spawn(async {
106 use rfd::AsyncFileDialog;
107
108 let file = AsyncFileDialog::new()
109 .add_filter("IFC Files", &["ifc", "IFC"])
110 .set_title("Open IFC File")
111 .pick_file()
112 .await;
113
114 file.map(|f| f.path().to_path_buf())
115 });
116
117 state.task = Some(task);
118 }
119}
120
121#[cfg(target_arch = "wasm32")]
123fn handle_open_dialog_request(
124 mut requests: MessageReader<OpenFileDialogRequest>,
125 _state: ResMut<FileDialogState>,
126) {
127 for _ in requests.read() {
128 crate::log_info("[Loader] Opening file dialog (WASM)...");
129 trigger_file_dialog();
130 }
131}
132
133#[cfg(all(not(target_arch = "wasm32"), target_os = "ios",))]
135fn handle_open_dialog_request(
136 mut _requests: MessageReader<OpenFileDialogRequest>,
137 mut _state: ResMut<FileDialogState>,
138) {
139 }
141
142fn poll_file_dialog(
144 mut state: ResMut<FileDialogState>,
145 mut load_events: MessageWriter<LoadIfcFileEvent>,
146) {
147 if let Some(ref mut task) = state.task {
148 if let Some(result) = bevy::tasks::block_on(bevy::tasks::poll_once(task)) {
149 if let Some(path) = result {
150 crate::log_info(&format!("[Loader] File selected: {:?}", path));
151 load_events.write(LoadIfcFileEvent { path });
152 } else {
153 crate::log("[Loader] File dialog cancelled");
154 }
155 state.task = None;
156 }
157 }
158}
159
160fn handle_load_file_event(
162 mut events: MessageReader<LoadIfcFileEvent>,
163 mut scene_data: ResMut<IfcSceneData>,
164 mut auto_fit: ResMut<crate::mesh::AutoFitState>,
165 mut loaded_events: MessageWriter<IfcFileLoadedEvent>,
166) {
167 for event in events.read() {
168 crate::log_info(&format!("[Loader] Loading file: {:?}", event.path));
169
170 match load_ifc_file(&event.path) {
171 Ok((meshes, entities)) => {
172 let mesh_count = meshes.len();
173 let entity_count = entities.len();
174
175 crate::log_info(&format!(
176 "[Loader] Loaded {} meshes, {} entities",
177 mesh_count, entity_count
178 ));
179
180 scene_data.meshes = meshes;
182 scene_data.entities = entities;
183 scene_data.dirty = true;
184 scene_data.bounds = None;
185
186 auto_fit.has_fit = false;
188
189 loaded_events.write(IfcFileLoadedEvent {
190 path: event.path.clone(),
191 entity_count,
192 mesh_count,
193 });
194 }
195 Err(e) => {
196 crate::log_info(&format!("[Loader] Error loading file: {}", e));
197 }
198 }
199 }
200}
201
202fn handle_file_drop(
204 mut file_drag_drop_events: MessageReader<bevy::window::FileDragAndDrop>,
205 mut load_events: MessageWriter<LoadIfcFileEvent>,
206) {
207 for event in file_drag_drop_events.read() {
208 if let bevy::window::FileDragAndDrop::DroppedFile { path_buf, .. } = event {
209 if let Some(ext) = path_buf.extension() {
211 if ext.eq_ignore_ascii_case("ifc") {
212 crate::log_info(&format!("[Loader] File dropped: {:?}", path_buf));
213 load_events.write(LoadIfcFileEvent {
214 path: path_buf.clone(),
215 });
216 }
217 }
218 }
219 }
220}
221
222fn handle_load_content_event(
224 mut events: MessageReader<LoadIfcContentEvent>,
225 mut scene_data: ResMut<IfcSceneData>,
226 mut auto_fit: ResMut<crate::mesh::AutoFitState>,
227 mut loaded_events: MessageWriter<IfcFileLoadedEvent>,
228) {
229 for event in events.read() {
230 crate::log_info(&format!(
231 "[Loader] Loading content: {} ({:.2} MB)",
232 event.file_name,
233 event.content.len() as f64 / (1024.0 * 1024.0)
234 ));
235
236 match load_ifc_content(&event.content) {
237 Ok((meshes, entities)) => {
238 let mesh_count = meshes.len();
239 let entity_count = entities.len();
240
241 crate::log_info(&format!(
242 "[Loader] Loaded {} meshes, {} entities",
243 mesh_count, entity_count
244 ));
245
246 scene_data.meshes = meshes;
248 scene_data.entities = entities;
249 scene_data.dirty = true;
250 scene_data.bounds = None;
251
252 auto_fit.has_fit = false;
254
255 loaded_events.write(IfcFileLoadedEvent {
256 path: PathBuf::from(&event.file_name),
257 entity_count,
258 mesh_count,
259 });
260 }
261 Err(e) => {
262 crate::log_info(&format!("[Loader] Error loading content: {}", e));
263 }
264 }
265 }
266}
267
268#[cfg(target_arch = "wasm32")]
273mod wasm_file_input {
274 use super::*;
275 use std::sync::Mutex;
276 use wasm_bindgen::closure::Closure;
277
278 static PENDING_FILE: Mutex<Option<(String, String)>> = Mutex::new(None);
280
281 pub fn setup_wasm_file_input() {
283 let window = match web_sys::window() {
285 Some(w) => w,
286 None => return,
287 };
288 let document = match window.document() {
289 Some(d) => d,
290 None => return,
291 };
292
293 let input: web_sys::HtmlInputElement = match document.create_element("input") {
295 Ok(el) => match el.dyn_into() {
296 Ok(i) => i,
297 Err(_) => return,
298 },
299 Err(_) => return,
300 };
301
302 input.set_type("file");
303 input.set_accept(".ifc,.IFC");
304 input.set_id("bevy-file-input");
305 input.style().set_property("display", "none").ok();
306
307 if let Some(body) = document.body() {
309 let _ = body.append_child(&input);
310 }
311
312 let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
314 let input: web_sys::HtmlInputElement = match event.target() {
315 Some(t) => match t.dyn_into() {
316 Ok(i) => i,
317 Err(_) => return,
318 },
319 None => return,
320 };
321
322 let files = match input.files() {
323 Some(f) => f,
324 None => return,
325 };
326
327 let file = match files.get(0) {
328 Some(f) => f,
329 None => return,
330 };
331
332 let file_name = file.name();
333 crate::log_info(&format!("[WASM] File selected: {}", file_name));
334
335 let reader = match web_sys::FileReader::new() {
337 Ok(r) => r,
338 Err(_) => return,
339 };
340
341 let reader_clone = reader.clone();
342 let file_name_clone = file_name.clone();
343
344 let onload = Closure::wrap(Box::new(move |_: web_sys::Event| {
345 let result = match reader_clone.result() {
346 Ok(r) => r,
347 Err(_) => return,
348 };
349
350 let content = match result.as_string() {
351 Some(s) => s,
352 None => return,
353 };
354
355 crate::log_info(&format!("[WASM] File read: {} bytes", content.len()));
356
357 if let Ok(mut pending) = PENDING_FILE.lock() {
359 *pending = Some((file_name_clone.clone(), content));
360 }
361 }) as Box<dyn FnMut(_)>);
362
363 reader.set_onload(Some(onload.as_ref().unchecked_ref()));
364 onload.forget(); let _ = reader.read_as_text(&file);
367
368 input.set_value("");
370 }) as Box<dyn FnMut(_)>);
371
372 input.set_onchange(Some(closure.as_ref().unchecked_ref()));
373 closure.forget(); crate::log("[WASM] File input element created");
376 }
377
378 pub fn poll_pending_file() -> Option<(String, String)> {
380 if let Ok(mut pending) = PENDING_FILE.lock() {
381 pending.take()
382 } else {
383 None
384 }
385 }
386
387 pub fn trigger_file_dialog() {
389 let window = match web_sys::window() {
390 Some(w) => w,
391 None => return,
392 };
393 let document = match window.document() {
394 Some(d) => d,
395 None => return,
396 };
397
398 if let Some(input) = document.get_element_by_id("bevy-file-input") {
399 if let Ok(input) = input.dyn_into::<web_sys::HtmlInputElement>() {
400 input.click();
401 }
402 }
403 }
404}
405
406#[cfg(target_arch = "wasm32")]
407pub use wasm_file_input::*;
408
409#[cfg(not(target_arch = "wasm32"))]
410#[allow(dead_code)]
411fn setup_wasm_file_input() {
412 }
414
415#[cfg(not(target_arch = "wasm32"))]
416pub fn poll_pending_file() -> Option<(String, String)> {
417 None
418}
419
420#[cfg(not(target_arch = "wasm32"))]
421pub fn trigger_file_dialog() {
422 }
424
425fn load_ifc_file(
427 path: &std::path::Path,
428) -> Result<(Vec<IfcMesh>, Vec<EntityInfo>), Box<dyn std::error::Error>> {
429 let content = std::fs::read_to_string(path)?;
431
432 let model = Arc::new(ParsedModel::parse(&content, false, false)?);
435
436 let unit_scale = model.unit_scale();
438 let router = GeometryRouter::with_default_processors_and_unit_scale(unit_scale);
439
440 let resolver = model.resolver();
442
443 let mut meshes = Vec::new();
445 let mut entities = Vec::new();
446
447 let mut scanner = EntityScanner::new(&content);
450 let mut element_ids: Vec<(u32, String)> = Vec::new();
451
452 while let Some((id, type_name, _, _)) = scanner.next_entity() {
453 if has_geometry_type_name(type_name) {
455 element_ids.push((id, type_name.to_string()));
456 }
457 }
458
459 crate::log_info(&format!(
460 "[Loader] Found {} building elements",
461 element_ids.len()
462 ));
463
464 let styled_colors = build_styled_item_colors(resolver);
466 if !styled_colors.is_empty() {
467 crate::log_info(&format!(
468 "[Loader] Found {} styled item colors",
469 styled_colors.len()
470 ));
471 }
472
473 for (id, type_name) in element_ids {
475 let entity = match resolver.get(EntityId(id)) {
477 Some(e) => e,
478 None => continue,
479 };
480
481 let name: Option<String> = entity.get_string(2).map(|s: &str| s.to_string());
483
484 let mesh = match router.process_element(&entity, resolver) {
486 Ok(m) => m,
487 Err(e) => {
488 crate::log(&format!(
489 "[Loader] Failed to process #{} ({}): {}",
490 id, type_name, e
491 ));
492 continue;
493 }
494 };
495
496 let color = get_entity_surface_color(&entity, resolver, &styled_colors)
498 .unwrap_or_else(|| crate::mesh::get_default_color(&type_name));
499
500 if mesh.is_empty() {
501 if is_point_entity_type(&type_name) {
503 if let Some(pos) = extract_entity_position(&entity, resolver, unit_scale) {
504 let marker = create_marker_sphere(pos, 0.5);
505 let ifc_mesh = IfcMesh::from_geometry_mesh(
506 id as u64,
507 marker,
508 color,
509 type_name.clone(),
510 name.clone(),
511 );
512 meshes.push(ifc_mesh);
513 entities.push(EntityInfo {
514 id: id as u64,
515 entity_type: type_name,
516 name,
517 storey: None,
518 storey_elevation: None,
519 });
520 }
521 }
522 continue;
523 }
524
525 let ifc_mesh = IfcMesh::from_geometry_mesh(
527 id as u64,
528 mesh, color,
530 type_name.clone(),
531 name.clone(),
532 );
533 meshes.push(ifc_mesh);
534
535 entities.push(EntityInfo {
537 id: id as u64,
538 entity_type: type_name,
539 name,
540 storey: None, storey_elevation: None,
542 });
543 }
544
545 Ok((meshes, entities))
546}
547
548fn load_ifc_content(
550 content: &str,
551) -> Result<(Vec<IfcMesh>, Vec<EntityInfo>), Box<dyn std::error::Error>> {
552 let model = Arc::new(ParsedModel::parse(content, false, false)?);
555
556 let unit_scale = model.unit_scale();
558 let router = GeometryRouter::with_default_processors_and_unit_scale(unit_scale);
559
560 let resolver = model.resolver();
562
563 let mut meshes = Vec::new();
565 let mut entities = Vec::new();
566
567 let mut scanner = EntityScanner::new(content);
570 let mut element_ids: Vec<(u32, String)> = Vec::new();
571
572 while let Some((id, type_name, _, _)) = scanner.next_entity() {
573 if has_geometry_type_name(type_name) {
575 element_ids.push((id, type_name.to_string()));
576 }
577 }
578
579 crate::log_info(&format!(
580 "[Loader] Found {} building elements",
581 element_ids.len()
582 ));
583
584 let styled_colors = build_styled_item_colors(resolver);
586 if !styled_colors.is_empty() {
587 crate::log_info(&format!(
588 "[Loader] Found {} styled item colors",
589 styled_colors.len()
590 ));
591 }
592
593 for (id, type_name) in element_ids {
595 let entity = match resolver.get(EntityId(id)) {
597 Some(e) => e,
598 None => continue,
599 };
600
601 let name: Option<String> = entity.get_string(2).map(|s: &str| s.to_string());
603
604 let mesh = match router.process_element(&entity, resolver) {
606 Ok(m) => m,
607 Err(e) => {
608 crate::log(&format!(
609 "[Loader] Failed to process #{} ({}): {}",
610 id, type_name, e
611 ));
612 continue;
613 }
614 };
615
616 let color = get_entity_surface_color(&entity, resolver, &styled_colors)
618 .unwrap_or_else(|| crate::mesh::get_default_color(&type_name));
619
620 if mesh.is_empty() {
621 if is_point_entity_type(&type_name) {
623 if let Some(pos) = extract_entity_position(&entity, resolver, unit_scale) {
624 let marker = create_marker_sphere(pos, 0.5);
625 let ifc_mesh = IfcMesh::from_geometry_mesh(
626 id as u64,
627 marker,
628 color,
629 type_name.clone(),
630 name.clone(),
631 );
632 meshes.push(ifc_mesh);
633 entities.push(EntityInfo {
634 id: id as u64,
635 entity_type: type_name,
636 name,
637 storey: None,
638 storey_elevation: None,
639 });
640 }
641 }
642 continue;
643 }
644
645 let ifc_mesh = IfcMesh::from_geometry_mesh(
647 id as u64,
648 mesh, color,
650 type_name.clone(),
651 name.clone(),
652 );
653 meshes.push(ifc_mesh);
654
655 entities.push(EntityInfo {
657 id: id as u64,
658 entity_type: type_name,
659 name,
660 storey: None, storey_elevation: None,
662 });
663 }
664
665 Ok((meshes, entities))
666}
667
668fn has_geometry_type_name(type_name: &str) -> bool {
671 matches!(
672 type_name.to_uppercase().as_str(),
673 "IFCWALL"
675 | "IFCWALLSTANDARDCASE"
676 | "IFCCURTAINWALL"
677 | "IFCSLAB"
679 | "IFCROOF"
681 | "IFCBEAM"
683 | "IFCCOLUMN"
684 | "IFCMEMBER"
685 | "IFCPLATE"
686 | "IFCDOOR"
688 | "IFCWINDOW"
689 | "IFCSTAIR"
691 | "IFCSTAIRFLIGHT"
692 | "IFCRAMP"
693 | "IFCRAMPFLIGHT"
694 | "IFCRAILING"
695 | "IFCCOVERING"
697 | "IFCFURNISHINGELEMENT"
699 | "IFCFOOTING"
701 | "IFCPILE"
702 | "IFCBUILDINGELEMENTPROXY"
704 | "IFCELEMENTASSEMBLY"
705 | "IFCFLOWTERMINAL"
707 | "IFCFLOWSEGMENT"
708 | "IFCFLOWFITTING"
709 | "IFCFLOWCONTROLLER"
710 | "IFCLIGHTFIXTURE"
712 | "IFCSPACE"
714 )
715}
716
717fn is_point_entity_type(type_name: &str) -> bool {
719 matches!(type_name.to_uppercase().as_str(), "IFCLIGHTFIXTURE")
720}
721
722fn extract_entity_position(
727 entity: &bimifc_model::DecodedEntity,
728 resolver: &dyn bimifc_model::EntityResolver,
729 unit_scale: f64,
730) -> Option<[f32; 3]> {
731 let placement_id = entity.get_ref(5)?; let transform = bimifc_geometry::transform::resolve_placement(placement_id, resolver)?;
733 Some([
734 (transform[(0, 3)] * unit_scale) as f32,
735 (transform[(1, 3)] * unit_scale) as f32,
736 (transform[(2, 3)] * unit_scale) as f32,
737 ])
738}
739
740fn create_marker_sphere(center: [f32; 3], radius: f32) -> bimifc_geometry::Mesh {
745 let stacks: u32 = 8;
746 let slices: u32 = 12;
747
748 let vertex_count = ((stacks + 1) * (slices + 1)) as usize;
749 let index_count = (stacks * slices * 6) as usize;
750
751 let mut positions = Vec::with_capacity(vertex_count * 3);
752 let mut normals = Vec::with_capacity(vertex_count * 3);
753 let mut indices = Vec::with_capacity(index_count);
754
755 for i in 0..=stacks {
757 let phi = std::f32::consts::PI * i as f32 / stacks as f32;
758 let sin_phi = phi.sin();
759 let cos_phi = phi.cos();
760
761 for j in 0..=slices {
762 let theta = 2.0 * std::f32::consts::PI * j as f32 / slices as f32;
763 let sin_theta = theta.sin();
764 let cos_theta = theta.cos();
765
766 let nx = sin_phi * cos_theta;
767 let ny = sin_phi * sin_theta;
768 let nz = cos_phi;
769
770 positions.push(center[0] + radius * nx);
771 positions.push(center[1] + radius * ny);
772 positions.push(center[2] + radius * nz);
773
774 normals.push(nx);
775 normals.push(ny);
776 normals.push(nz);
777 }
778 }
779
780 for i in 0..stacks {
782 for j in 0..slices {
783 let row_start = i * (slices + 1);
784 let next_row = (i + 1) * (slices + 1);
785
786 let tl = row_start + j;
787 let tr = row_start + j + 1;
788 let bl = next_row + j;
789 let br = next_row + j + 1;
790
791 indices.push(tl);
792 indices.push(bl);
793 indices.push(tr);
794
795 indices.push(tr);
796 indices.push(bl);
797 indices.push(br);
798 }
799 }
800
801 bimifc_geometry::Mesh {
802 positions,
803 normals,
804 indices,
805 }
806}
807
808fn build_styled_item_colors(resolver: &dyn EntityResolver) -> FxHashMap<u32, [f32; 4]> {
816 let mut color_map = FxHashMap::default();
817
818 for styled_item in resolver.entities_by_type(&IfcType::IfcStyledItem) {
819 let item_id = match styled_item.get_ref(0) {
821 Some(id) => id,
822 None => continue,
823 };
824
825 let styles = match styled_item.get(1) {
827 Some(AttributeValue::List(list)) => list,
828 _ => continue,
829 };
830
831 let surface_style_id = match styles.first().and_then(|v| v.as_entity_ref()) {
833 Some(id) => id,
834 None => continue,
835 };
836 let surface_style = match resolver.get(surface_style_id) {
837 Some(e) => e,
838 None => continue,
839 };
840
841 let sub_styles = match surface_style.get(2) {
843 Some(AttributeValue::List(list)) => list,
844 _ => continue,
845 };
846
847 let rendering_id = match sub_styles.first().and_then(|v| v.as_entity_ref()) {
848 Some(id) => id,
849 None => continue,
850 };
851 let rendering = match resolver.get(rendering_id) {
852 Some(e) => e,
853 None => continue,
854 };
855
856 let colour_id = match rendering.get_ref(0) {
858 Some(id) => id,
859 None => continue,
860 };
861 let colour = match resolver.get(colour_id) {
862 Some(e) => e,
863 None => continue,
864 };
865
866 let r = colour.get_float(1).unwrap_or(0.7) as f32;
868 let g = colour.get_float(2).unwrap_or(0.7) as f32;
869 let b = colour.get_float(3).unwrap_or(0.7) as f32;
870
871 color_map.insert(item_id.0, [r, g, b, 1.0]);
872 }
873
874 color_map
875}
876
877fn get_entity_surface_color(
882 entity: &bimifc_model::DecodedEntity,
883 resolver: &dyn EntityResolver,
884 styled_colors: &FxHashMap<u32, [f32; 4]>,
885) -> Option<[f32; 4]> {
886 let rep_id = entity.get_ref(6)?;
888 let representation = resolver.get(rep_id)?;
889
890 let reps = match representation.get(2) {
892 Some(AttributeValue::List(list)) => list,
893 _ => return None,
894 };
895
896 for rep_ref in reps {
897 let shape_rep_id = rep_ref.as_entity_ref()?;
898 let shape_rep = resolver.get(shape_rep_id)?;
899
900 let items = match shape_rep.get(3) {
902 Some(AttributeValue::List(list)) => list,
903 _ => continue,
904 };
905
906 for item_ref in items {
907 if let Some(item_id) = item_ref.as_entity_ref() {
908 if let Some(color) = styled_colors.get(&item_id.0) {
909 return Some(*color);
910 }
911 }
912 }
913 }
914
915 None
916}