1use crate::{log, IfcSceneData, SceneBounds};
21use bevy::asset::RenderAssetUsages;
22#[cfg(feature = "color-palette")]
23use bevy::mesh::VertexAttributeValues;
24use bevy::mesh::{Indices, PrimitiveTopology};
25use bevy::prelude::*;
26use serde::{Deserialize, Serialize};
27use std::sync::Arc;
28
29pub struct MeshPlugin;
31
32impl Plugin for MeshPlugin {
33 fn build(&self, app: &mut App) {
34 app.init_resource::<AutoFitState>()
35 .init_resource::<PendingFocus>()
36 .init_resource::<TriangleEntityMapping>()
37 .init_resource::<CurrentPalette>()
38 .init_resource::<EntityColorMapping>()
39 .init_resource::<PreviousSelection>()
40 .init_resource::<PreviousVisibility>()
41 .add_systems(
42 Update,
43 (
44 poll_palette_change_system,
45 spawn_meshes_system,
46 auto_fit_camera_system,
47 update_mesh_visibility_system,
48 update_mesh_selection_system,
49 poll_focus_command_system,
50 )
51 .chain(),
52 );
53 }
54}
55
56#[derive(Resource, Default)]
58pub struct PendingFocus {
59 pub entity_id: Option<u64>,
60}
61
62#[derive(Resource, Default)]
64pub struct AutoFitState {
65 pub has_fit: bool,
67}
68
69#[cfg(feature = "color-palette")]
71#[derive(Resource, Default)]
72pub struct CurrentPalette {
73 pub palette: ColorPalette,
74}
75
76#[cfg(not(feature = "color-palette"))]
77#[derive(Resource, Default)]
78pub struct CurrentPalette;
79
80#[cfg(feature = "color-palette")]
82#[derive(Clone, Debug)]
83pub struct EntityColorInfo {
84 pub entity_id: u64,
85 pub entity_type: String,
86 pub original_color: [f32; 4],
87 pub has_ifc_color: bool,
89 pub start_vertex: u32,
90 pub vertex_count: u32,
91}
92
93#[cfg(feature = "color-palette")]
95#[derive(Resource, Default)]
96pub struct EntityColorMapping {
97 pub opaque: Vec<EntityColorInfo>,
99 pub transparent: Vec<EntityColorInfo>,
101}
102
103#[cfg(not(feature = "color-palette"))]
104#[derive(Resource, Default)]
105pub struct EntityColorMapping;
106
107#[derive(Clone, Debug, Default)]
113pub struct MeshGeometry {
114 pub positions: Vec<f32>,
116 pub normals: Vec<f32>,
118 pub indices: Vec<u32>,
120}
121
122impl MeshGeometry {
123 pub fn new(positions: Vec<f32>, normals: Vec<f32>, indices: Vec<u32>) -> Self {
125 Self {
126 positions,
127 normals,
128 indices,
129 }
130 }
131
132 pub fn from_geometry_mesh(mesh: bimifc_geometry::Mesh) -> Self {
134 Self {
135 positions: mesh.positions,
136 normals: mesh.normals,
137 indices: mesh.indices,
138 }
139 }
140
141 pub fn vertex_count(&self) -> usize {
143 self.positions.len() / 3
144 }
145
146 pub fn triangle_count(&self) -> usize {
148 self.indices.len() / 3
149 }
150
151 pub fn is_empty(&self) -> bool {
153 self.positions.is_empty()
154 }
155}
156
157#[derive(Clone, Debug)]
163pub struct IfcMesh {
164 pub entity_id: u64,
166 pub geometry: Arc<MeshGeometry>,
168 pub color: [f32; 4],
170 pub transform: [f32; 16],
172 pub entity_type: String,
174 pub name: Option<String>,
176 pub has_ifc_color: bool,
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
183pub struct IfcMeshSerialized {
184 pub entity_id: u64,
186 pub positions: Vec<f32>,
188 pub normals: Vec<f32>,
190 pub indices: Vec<u32>,
192 pub color: [f32; 4],
194 pub transform: [f32; 16],
196 pub entity_type: String,
198 pub name: Option<String>,
200}
201
202impl From<IfcMeshSerialized> for IfcMesh {
203 fn from(s: IfcMeshSerialized) -> Self {
204 Self {
205 entity_id: s.entity_id,
206 geometry: Arc::new(MeshGeometry::new(s.positions, s.normals, s.indices)),
207 color: s.color,
208 transform: s.transform,
209 entity_type: s.entity_type,
210 name: s.name,
211 has_ifc_color: false,
212 }
213 }
214}
215
216impl From<&IfcMesh> for IfcMeshSerialized {
217 fn from(m: &IfcMesh) -> Self {
218 Self {
219 entity_id: m.entity_id,
220 positions: m.geometry.positions.clone(),
221 normals: m.geometry.normals.clone(),
222 indices: m.geometry.indices.clone(),
223 color: m.color,
224 transform: m.transform,
225 entity_type: m.entity_type.clone(),
226 name: m.name.clone(),
227 }
228 }
229}
230
231impl IfcMesh {
232 pub fn new(
234 entity_id: u64,
235 geometry: Arc<MeshGeometry>,
236 color: [f32; 4],
237 transform: [f32; 16],
238 entity_type: String,
239 name: Option<String>,
240 ) -> Self {
241 Self {
242 entity_id,
243 geometry,
244 color,
245 transform,
246 entity_type,
247 name,
248 has_ifc_color: false,
249 }
250 }
251
252 pub fn from_geometry_mesh(
254 entity_id: u64,
255 mesh: bimifc_geometry::Mesh,
256 color: [f32; 4],
257 entity_type: String,
258 name: Option<String>,
259 ) -> Self {
260 Self {
261 entity_id,
262 geometry: Arc::new(MeshGeometry::from_geometry_mesh(mesh)),
263 color,
264 transform: [
265 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ],
270 entity_type,
271 name,
272 has_ifc_color: false,
273 }
274 }
275
276 pub fn is_empty(&self) -> bool {
278 self.geometry.is_empty()
279 }
280
281 pub fn to_bevy_mesh(&self) -> Mesh {
283 let vertex_count = self.geometry.vertex_count();
284
285 let positions: Vec<[f32; 3]> = (0..vertex_count)
287 .map(|i| {
288 let idx = i * 3;
289 [
291 self.geometry.positions[idx], self.geometry.positions[idx + 2], -self.geometry.positions[idx + 1], ]
295 })
296 .collect();
297
298 let normals: Vec<[f32; 3]> = if self.geometry.normals.len() == self.geometry.positions.len()
300 {
301 (0..vertex_count)
302 .map(|i| {
303 let idx = i * 3;
304 [
305 self.geometry.normals[idx],
306 self.geometry.normals[idx + 2],
307 -self.geometry.normals[idx + 1],
308 ]
309 })
310 .collect()
311 } else {
312 compute_flat_normals(&positions, &self.geometry.indices)
314 };
315
316 let mut mesh = Mesh::new(
317 PrimitiveTopology::TriangleList,
318 RenderAssetUsages::default(),
319 );
320
321 mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
322 mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
323 mesh.insert_indices(Indices::U32(self.geometry.indices.clone()));
324
325 mesh
326 }
327
328 pub fn get_transform(&self) -> Transform {
330 let mat = Mat4::from_cols_array(&self.transform);
331 Transform::from_matrix(mat)
332 }
333
334 pub fn get_color(&self) -> Color {
336 Color::srgba(self.color[0], self.color[1], self.color[2], self.color[3])
337 }
338}
339
340#[derive(Component)]
342pub struct IfcEntity {
343 pub id: u64,
344 pub entity_type: String,
345 pub name: Option<String>,
346}
347
348#[derive(Component, Clone, Debug)]
350pub struct EntityBounds {
351 pub min: Vec3,
352 pub max: Vec3,
353}
354
355impl EntityBounds {
356 pub fn center(&self) -> Vec3 {
357 (self.min + self.max) * 0.5
358 }
359
360 pub fn diagonal(&self) -> f32 {
361 (self.max - self.min).length()
362 }
363}
364
365#[derive(Component)]
367pub struct NeedsMaterialUpdate;
368
369#[derive(Component)]
371pub struct BatchedMesh {
372 pub is_transparent: bool,
374}
375
376#[derive(Resource, Default)]
378pub struct TriangleEntityMapping {
379 pub opaque: Vec<u64>,
381 pub transparent: Vec<u64>,
383}
384
385impl TriangleEntityMapping {
386 pub fn get_entity(&self, is_transparent: bool, triangle_index: usize) -> Option<u64> {
388 let mapping = if is_transparent {
389 &self.transparent
390 } else {
391 &self.opaque
392 };
393 mapping.get(triangle_index).copied()
394 }
395}
396
397struct BatchBuilder {
399 positions: Vec<[f32; 3]>,
400 normals: Vec<[f32; 3]>,
401 colors: Vec<[f32; 4]>,
402 indices: Vec<u32>,
403 triangle_to_entity: Vec<u64>,
405 #[cfg(feature = "color-palette")]
407 entity_color_info: Vec<EntityColorInfo>,
408}
409
410impl BatchBuilder {
411 fn with_capacity(vertex_hint: usize, index_hint: usize) -> Self {
412 Self {
413 positions: Vec::with_capacity(vertex_hint),
414 normals: Vec::with_capacity(vertex_hint),
415 colors: Vec::with_capacity(vertex_hint),
416 indices: Vec::with_capacity(index_hint),
417 triangle_to_entity: Vec::with_capacity(index_hint / 3),
418 #[cfg(feature = "color-palette")]
419 entity_color_info: Vec::new(),
420 }
421 }
422
423 fn add_mesh(&mut self, ifc_mesh: &IfcMesh) {
425 let geometry = &ifc_mesh.geometry;
426 let vertex_count = geometry.vertex_count();
427 if vertex_count == 0 {
428 return;
429 }
430
431 let start_vertex = self.positions.len();
432 let transform = ifc_mesh.get_transform();
433 let color = [
434 ifc_mesh.color[0],
435 ifc_mesh.color[1],
436 ifc_mesh.color[2],
437 ifc_mesh.color[3],
438 ];
439
440 for i in 0..vertex_count {
442 let idx = i * 3;
443 let local_pos = Vec3::new(
445 geometry.positions[idx],
446 geometry.positions[idx + 2], -geometry.positions[idx + 1], );
449 let world_pos = transform.transform_point(local_pos);
450 self.positions.push([world_pos.x, world_pos.y, world_pos.z]);
451
452 if geometry.normals.len() == geometry.positions.len() {
454 let local_normal = Vec3::new(
455 geometry.normals[idx],
456 geometry.normals[idx + 2],
457 -geometry.normals[idx + 1],
458 );
459 let world_normal = transform.rotation * local_normal;
460 self.normals
461 .push([world_normal.x, world_normal.y, world_normal.z]);
462 } else {
463 self.normals.push([0.0, 1.0, 0.0]); }
465
466 self.colors.push(color);
467 }
468
469 let index_offset = start_vertex as u32;
471 let num_triangles = geometry.triangle_count();
472 for &idx in &geometry.indices {
473 self.indices.push(idx + index_offset);
474 }
475
476 for _ in 0..num_triangles {
478 self.triangle_to_entity.push(ifc_mesh.entity_id);
479 }
480
481 #[cfg(feature = "color-palette")]
483 self.entity_color_info.push(EntityColorInfo {
484 entity_id: ifc_mesh.entity_id,
485 entity_type: ifc_mesh.entity_type.clone(),
486 original_color: color,
487 has_ifc_color: ifc_mesh.has_ifc_color,
488 start_vertex: start_vertex as u32,
489 vertex_count: vertex_count as u32,
490 });
491 }
492
493 fn take_triangle_mapping(&mut self) -> Vec<u64> {
495 std::mem::take(&mut self.triangle_to_entity)
496 }
497
498 #[cfg(feature = "color-palette")]
500 fn take_color_info(&mut self) -> Vec<EntityColorInfo> {
501 std::mem::take(&mut self.entity_color_info)
502 }
503
504 fn build(self) -> Mesh {
506 let usage = RenderAssetUsages::default();
507 let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, usage);
508
509 let normals = if self.normals.iter().all(|n| n[1] == 1.0 && n[0] == 0.0) {
511 compute_flat_normals(&self.positions, &self.indices)
512 } else {
513 self.normals
514 };
515
516 mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, self.positions);
517 mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
518 mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, self.colors);
519 mesh.insert_indices(Indices::U32(self.indices));
520
521 mesh
522 }
523
524 fn is_empty(&self) -> bool {
525 self.positions.is_empty()
526 }
527
528 fn vertex_count(&self) -> usize {
529 self.positions.len()
530 }
531
532 fn triangle_count(&self) -> usize {
533 self.indices.len() / 3
534 }
535}
536
537#[cfg(target_arch = "wasm32")]
539fn now_ms() -> f64 {
540 js_sys::Date::now()
541}
542
543#[cfg(not(target_arch = "wasm32"))]
544fn now_ms() -> f64 {
545 use std::time::{SystemTime, UNIX_EPOCH};
546 SystemTime::now()
547 .duration_since(UNIX_EPOCH)
548 .map(|d| d.as_millis() as f64)
549 .unwrap_or(0.0)
550}
551
552#[allow(clippy::too_many_arguments, unused_mut)]
554fn spawn_meshes_system(
555 mut commands: Commands,
556 mut meshes: ResMut<Assets<Mesh>>,
557 mut materials: ResMut<Assets<StandardMaterial>>,
558 mut scene_data: ResMut<IfcSceneData>,
559 mut triangle_mapping: ResMut<TriangleEntityMapping>,
560 mut color_mapping: ResMut<EntityColorMapping>,
561 existing_entities: Query<Entity, With<IfcEntity>>,
562 existing_batches: Query<Entity, With<BatchedMesh>>,
563) {
564 if !scene_data.dirty {
565 return;
566 }
567
568 let batch_start = now_ms();
569 let mesh_count = scene_data.meshes.len();
570 crate::log_info(&format!("[Bevy] Batching {} meshes for GPU...", mesh_count));
571
572 triangle_mapping.opaque.clear();
574 triangle_mapping.transparent.clear();
575 #[cfg(feature = "color-palette")]
576 {
577 color_mapping.opaque.clear();
578 color_mapping.transparent.clear();
579 }
580 let _ = &color_mapping; for entity in existing_entities.iter() {
584 commands.entity(entity).despawn();
585 }
586 for entity in existing_batches.iter() {
587 commands.entity(entity).despawn();
588 }
589
590 let vertex_hint = mesh_count * 100;
592 let index_hint = mesh_count * 300;
593
594 let mut opaque_batch = BatchBuilder::with_capacity(vertex_hint, index_hint);
595 let mut metallic_batch = BatchBuilder::with_capacity(vertex_hint / 5, index_hint / 5);
596 let mut transparent_batch = BatchBuilder::with_capacity(vertex_hint / 10, index_hint / 10);
597
598 let mut scene_min = Vec3::splat(f32::INFINITY);
600 let mut scene_max = Vec3::splat(f32::NEG_INFINITY);
601
602 for ifc_mesh in &scene_data.meshes {
604 let is_transparent = ifc_mesh.color[3] < 1.0;
605 let transform = ifc_mesh.get_transform();
606 let geometry = &ifc_mesh.geometry;
607
608 let mut entity_min = Vec3::splat(f32::INFINITY);
610 let mut entity_max = Vec3::splat(f32::NEG_INFINITY);
611 for i in (0..geometry.positions.len()).step_by(3) {
612 let pos = Vec3::new(
613 geometry.positions[i],
614 geometry.positions[i + 2],
615 -geometry.positions[i + 1],
616 );
617 let world_pos = transform.transform_point(pos);
618 entity_min = entity_min.min(world_pos);
619 entity_max = entity_max.max(world_pos);
620 scene_min = scene_min.min(world_pos);
621 scene_max = scene_max.max(world_pos);
622 }
623
624 let etype = ifc_mesh.entity_type.to_uppercase();
626 let is_metallic = etype.contains("BEAM")
627 || etype.contains("COLUMN")
628 || etype.contains("RAILING")
629 || (etype.contains("ROOF") && !etype.contains("SLAB"));
630
631 if is_transparent {
633 transparent_batch.add_mesh(ifc_mesh);
634 } else if is_metallic {
635 metallic_batch.add_mesh(ifc_mesh);
636 } else {
637 opaque_batch.add_mesh(ifc_mesh);
638 }
639
640 commands.spawn((
642 IfcEntity {
643 id: ifc_mesh.entity_id,
644 entity_type: ifc_mesh.entity_type.clone(),
645 name: ifc_mesh.name.clone(),
646 },
647 EntityBounds {
648 min: entity_min,
649 max: entity_max,
650 },
651 Transform::default(),
652 Visibility::default(),
653 ));
654 }
655
656 if !opaque_batch.is_empty() {
658 log(&format!(
659 "[Bevy] Opaque batch: {} vertices, {} triangles",
660 opaque_batch.vertex_count(),
661 opaque_batch.triangle_count()
662 ));
663
664 triangle_mapping.opaque = opaque_batch.take_triangle_mapping();
666 #[cfg(feature = "color-palette")]
667 {
668 color_mapping.opaque = opaque_batch.take_color_info();
669 }
670
671 let mesh = opaque_batch.build();
672 let material = StandardMaterial {
673 base_color: Color::WHITE,
674 metallic: 0.05,
675 perceptual_roughness: 0.45,
676 reflectance: 0.4,
677 double_sided: true,
678 cull_mode: None,
679 ..default()
680 };
681
682 commands.spawn((
683 Mesh3d(meshes.add(mesh)),
684 MeshMaterial3d(materials.add(material)),
685 Transform::default(),
686 BatchedMesh {
687 is_transparent: false,
688 },
689 ));
690 }
691
692 if !metallic_batch.is_empty() {
694 log(&format!(
695 "[Bevy] Metallic batch: {} vertices, {} triangles",
696 metallic_batch.vertex_count(),
697 metallic_batch.triangle_count()
698 ));
699
700 triangle_mapping
701 .opaque
702 .extend(metallic_batch.take_triangle_mapping());
703
704 let mesh = metallic_batch.build();
705 let material = StandardMaterial {
706 base_color: Color::WHITE,
707 metallic: 0.7,
708 perceptual_roughness: 0.25,
709 reflectance: 0.6,
710 double_sided: true,
711 cull_mode: None,
712 ..default()
713 };
714
715 commands.spawn((
716 Mesh3d(meshes.add(mesh)),
717 MeshMaterial3d(materials.add(material)),
718 Transform::default(),
719 BatchedMesh {
720 is_transparent: false,
721 },
722 ));
723 }
724
725 if !transparent_batch.is_empty() {
727 log(&format!(
728 "[Bevy] Transparent batch: {} vertices, {} triangles",
729 transparent_batch.vertex_count(),
730 transparent_batch.triangle_count()
731 ));
732
733 triangle_mapping.transparent = transparent_batch.take_triangle_mapping();
735 #[cfg(feature = "color-palette")]
736 {
737 color_mapping.transparent = transparent_batch.take_color_info();
738 }
739
740 let mesh = transparent_batch.build();
741 let material = StandardMaterial {
742 base_color: Color::WHITE,
743 metallic: 0.02,
744 perceptual_roughness: 0.08,
745 reflectance: 0.6,
746 double_sided: true,
747 cull_mode: None,
748 alpha_mode: AlphaMode::Blend,
749 ..default()
750 };
751
752 commands.spawn((
753 Mesh3d(meshes.add(mesh)),
754 MeshMaterial3d(materials.add(material)),
755 Transform::default(),
756 BatchedMesh {
757 is_transparent: true,
758 },
759 ));
760 }
761
762 if scene_min.x.is_finite() && scene_max.x.is_finite() {
764 scene_data.bounds = Some(SceneBounds {
765 min: scene_min,
766 max: scene_max,
767 });
768 log(&format!(
769 "[Bevy] Scene bounds: {:?} to {:?}",
770 scene_min, scene_max
771 ));
772 }
773
774 let total_vertices = scene_data
776 .meshes
777 .iter()
778 .map(|m| m.geometry.vertex_count())
779 .sum::<usize>();
780 let total_triangles = scene_data
781 .meshes
782 .iter()
783 .map(|m| m.geometry.triangle_count())
784 .sum::<usize>();
785 let geometry_size: usize = scene_data
786 .meshes
787 .iter()
788 .map(|m| {
789 m.geometry.positions.len() * 4
790 + m.geometry.normals.len() * 4
791 + m.geometry.indices.len() * 4
792 })
793 .sum();
794
795 let batch_time = now_ms() - batch_start;
796 crate::log_info(&format!(
797 "[Bevy] ✓ GPU upload: {:.0}ms | {} vertices, {} triangles | {:.1} MB geometry",
798 batch_time,
799 total_vertices,
800 total_triangles,
801 geometry_size as f64 / (1024.0 * 1024.0)
802 ));
803
804 for mesh in &mut scene_data.meshes {
806 mesh.geometry = Arc::new(MeshGeometry::default());
807 }
808
809 log(&format!(
810 "[Bevy] Freed {}MB of geometry data from IfcSceneData",
811 geometry_size / (1024 * 1024)
812 ));
813
814 scene_data.dirty = false;
815}
816
817fn auto_fit_camera_system(
819 scene_data: Res<IfcSceneData>,
820 mut auto_fit: ResMut<AutoFitState>,
821 mut camera_controller: ResMut<crate::camera::CameraController>,
822) {
823 if auto_fit.has_fit {
825 return;
826 }
827
828 if let Some(ref bounds) = scene_data.bounds {
829 log(&format!(
830 "[Bevy] Auto-fitting camera to bounds: {:?} to {:?}",
831 bounds.min, bounds.max
832 ));
833
834 let center = bounds.center();
836 let diagonal = bounds.diagonal();
837
838 let fov_rad = camera_controller.fov.to_radians();
840 let distance = diagonal / (2.0 * (fov_rad / 2.0).tan());
841
842 camera_controller.target = center;
844 camera_controller.distance = distance.max(100.0); camera_controller.azimuth = 0.785; camera_controller.elevation = 0.615; log(&format!(
849 "[Bevy] Camera set to: target={:?}, distance={}",
850 center, distance
851 ));
852
853 auto_fit.has_fit = true;
854 }
855}
856
857#[cfg(feature = "color-palette")]
861#[allow(unused_variables, unused_mut)]
862fn update_mesh_visibility_system(
863 mut previous_visibility: ResMut<PreviousVisibility>,
864 selection: Res<crate::picking::SelectionState>,
865 color_mapping: Res<EntityColorMapping>,
866 mut mesh_assets: ResMut<Assets<Mesh>>,
867 batched_meshes: Query<(&Mesh3d, &BatchedMesh)>,
868) {
869 #[cfg(target_arch = "wasm32")]
870 {
871 let vis = crate::storage::load_visibility();
872
873 let (new_hidden, new_isolated) = match &vis {
874 Some(v) => {
875 let hidden: rustc_hash::FxHashSet<u64> = v.hidden.iter().copied().collect();
876 let isolated: Option<rustc_hash::FxHashSet<u64>> =
877 v.isolated.as_ref().map(|i| i.iter().copied().collect());
878 (hidden, isolated)
879 }
880 None => (rustc_hash::FxHashSet::default(), None),
881 };
882
883 if new_hidden == previous_visibility.hidden && new_isolated == previous_visibility.isolated
885 {
886 return;
887 }
888
889 log(&format!(
890 "[Bevy] Visibility changed: {} hidden, isolated={:?}",
891 new_hidden.len(),
892 new_isolated.as_ref().map(|s| s.len()),
893 ));
894
895 let current_selection = &selection.selected;
896
897 for (mesh_handle, batched_mesh) in batched_meshes.iter() {
899 let Some(mesh) = mesh_assets.get_mut(&mesh_handle.0) else {
900 continue;
901 };
902
903 let Some(VertexAttributeValues::Float32x4(colors)) =
904 mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR)
905 else {
906 continue;
907 };
908
909 let color_infos = if batched_mesh.is_transparent {
910 &color_mapping.transparent
911 } else {
912 &color_mapping.opaque
913 };
914
915 for info in color_infos {
916 let visible = if let Some(ref iso) = new_isolated {
917 iso.contains(&info.entity_id)
918 } else {
919 !new_hidden.contains(&info.entity_id)
920 };
921
922 let start = info.start_vertex as usize;
923 let end = start + info.vertex_count as usize;
924
925 if visible {
926 let color = if current_selection.contains(&info.entity_id) {
928 SELECTION_COLOR
929 } else {
930 info.original_color
931 };
932 for c in colors[start..end].iter_mut() {
933 *c = color;
934 }
935 } else {
936 for c in colors[start..end].iter_mut() {
938 *c = [c[0], c[1], c[2], 0.0];
939 }
940 }
941 }
942 }
943
944 previous_visibility.hidden = new_hidden;
945 previous_visibility.isolated = new_isolated;
946 }
947}
948
949#[cfg(not(feature = "color-palette"))]
950fn update_mesh_visibility_system() {}
951
952#[cfg(feature = "color-palette")]
954#[derive(Resource, Default)]
955pub struct PreviousVisibility {
956 pub hidden: rustc_hash::FxHashSet<u64>,
957 pub isolated: Option<rustc_hash::FxHashSet<u64>>,
958}
959
960#[cfg(not(feature = "color-palette"))]
961#[derive(Resource, Default)]
962pub struct PreviousVisibility;
963
964#[cfg(feature = "color-palette")]
966const SELECTION_COLOR: [f32; 4] = [0.3, 0.7, 1.0, 1.0];
967
968#[derive(Resource, Default)]
970pub struct PreviousSelection {
971 #[cfg(feature = "color-palette")]
972 pub selected_ids: rustc_hash::FxHashSet<u64>,
973}
974
975#[cfg(feature = "color-palette")]
977fn update_mesh_selection_system(
978 selection: Res<crate::picking::SelectionState>,
979 mut previous_selection: ResMut<PreviousSelection>,
980 color_mapping: Res<EntityColorMapping>,
981 mut mesh_assets: ResMut<Assets<Mesh>>,
982 batched_meshes: Query<(&Mesh3d, &BatchedMesh)>,
983) {
984 if !selection.is_changed() {
985 return;
986 }
987
988 let current_selection = &selection.selected;
989
990 let newly_selected: Vec<u64> = current_selection
992 .difference(&previous_selection.selected_ids)
993 .copied()
994 .collect();
995 let newly_deselected: Vec<u64> = previous_selection
996 .selected_ids
997 .difference(current_selection)
998 .copied()
999 .collect();
1000
1001 if newly_selected.is_empty() && newly_deselected.is_empty() {
1002 return;
1003 }
1004
1005 for (mesh_handle, batched_mesh) in batched_meshes.iter() {
1007 let Some(mesh) = mesh_assets.get_mut(&mesh_handle.0) else {
1008 continue;
1009 };
1010
1011 let Some(VertexAttributeValues::Float32x4(colors)) =
1012 mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR)
1013 else {
1014 continue;
1015 };
1016
1017 let color_infos = if batched_mesh.is_transparent {
1018 &color_mapping.transparent
1019 } else {
1020 &color_mapping.opaque
1021 };
1022
1023 for &entity_id in &newly_selected {
1025 for info in color_infos.iter().filter(|i| i.entity_id == entity_id) {
1026 let start = info.start_vertex as usize;
1027 let end = start + info.vertex_count as usize;
1028 for color in colors[start..end].iter_mut() {
1029 *color = SELECTION_COLOR;
1030 }
1031 }
1032 }
1033
1034 for &entity_id in &newly_deselected {
1036 for info in color_infos.iter().filter(|i| i.entity_id == entity_id) {
1037 let start = info.start_vertex as usize;
1038 let end = start + info.vertex_count as usize;
1039 for color in colors[start..end].iter_mut() {
1040 *color = info.original_color;
1041 }
1042 }
1043 }
1044 }
1045
1046 previous_selection.selected_ids = current_selection.clone();
1048}
1049
1050#[cfg(not(feature = "color-palette"))]
1051fn update_mesh_selection_system(_selection: Res<crate::picking::SelectionState>) {
1052 }
1054
1055#[allow(unused_variables, unused_mut)]
1057fn poll_focus_command_system(
1058 mut camera_controller: ResMut<crate::camera::CameraController>,
1059 entities: Query<(&IfcEntity, &EntityBounds)>,
1060) {
1061 #[cfg(target_arch = "wasm32")]
1062 {
1063 if let Some(focus) = crate::storage::load_focus() {
1064 crate::storage::clear_focus();
1066
1067 log(&format!(
1068 "[Bevy] Focus command received for entity #{}",
1069 focus.entity_id
1070 ));
1071
1072 for (ifc_entity, bounds) in entities.iter() {
1074 if ifc_entity.id == focus.entity_id {
1075 log(&format!(
1076 "[Bevy] Focusing on entity '{}' ({}), bounds: {:?} to {:?}",
1077 ifc_entity.name.as_deref().unwrap_or("unnamed"),
1078 ifc_entity.entity_type,
1079 bounds.min,
1080 bounds.max
1081 ));
1082
1083 camera_controller.frame(bounds.min, bounds.max);
1085 break;
1086 }
1087 }
1088 }
1089 }
1090}
1091
1092#[cfg(feature = "color-palette")]
1094#[allow(unused_variables, unused_mut)]
1095fn poll_palette_change_system(
1096 mut current_palette: ResMut<CurrentPalette>,
1097 color_mapping: Res<EntityColorMapping>,
1098 mut mesh_assets: ResMut<Assets<Mesh>>,
1099 batched_meshes: Query<(&Mesh3d, &BatchedMesh)>,
1100) {
1101 #[cfg(target_arch = "wasm32")]
1102 {
1103 if let Some(palette_str) = crate::storage::load_palette() {
1104 crate::storage::clear_palette();
1106
1107 let new_palette = match palette_str.as_str() {
1109 "vibrant" => ColorPalette::Vibrant,
1110 "realistic" => ColorPalette::Realistic,
1111 "high_contrast" => ColorPalette::HighContrast,
1112 "monochrome" => ColorPalette::Monochrome,
1113 _ => {
1114 log(&format!("[Bevy] Unknown palette: {}", palette_str));
1115 return;
1116 }
1117 };
1118
1119 if current_palette.palette != new_palette {
1121 log(&format!(
1122 "[Bevy] Palette changed to {:?}, updating vertex colors in-place",
1123 new_palette
1124 ));
1125
1126 current_palette.palette = new_palette;
1127
1128 for (mesh_handle, batched) in batched_meshes.iter() {
1130 let mapping = if batched.is_transparent {
1131 &color_mapping.transparent
1132 } else {
1133 &color_mapping.opaque
1134 };
1135
1136 if let Some(mesh) = mesh_assets.get_mut(&mesh_handle.0) {
1137 if let Some(bevy::mesh::VertexAttributeValues::Float32x4(colors)) =
1139 mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR)
1140 {
1141 for info in mapping {
1143 let new_color = if info.has_ifc_color {
1145 info.original_color
1146 } else {
1147 get_color_for_palette(&info.entity_type, new_palette)
1148 };
1149 let start = info.start_vertex as usize;
1150 let end = start + info.vertex_count as usize;
1151 for i in start..end.min(colors.len()) {
1152 colors[i] = new_color;
1153 }
1154 }
1155 log(&format!(
1156 "[Bevy] Updated {} entity colors in {} batch",
1157 mapping.len(),
1158 if batched.is_transparent {
1159 "transparent"
1160 } else {
1161 "opaque"
1162 }
1163 ));
1164 }
1165 }
1166 }
1167 }
1168 }
1169 }
1170}
1171
1172#[cfg(not(feature = "color-palette"))]
1174fn poll_palette_change_system() {
1175 }
1177
1178#[cfg(feature = "color-palette")]
1180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1181pub enum ColorPalette {
1182 #[default]
1184 Vibrant,
1185 Realistic,
1187 HighContrast,
1189 Monochrome,
1191}
1192
1193#[cfg(feature = "color-palette")]
1194impl ColorPalette {
1195 pub fn all() -> &'static [ColorPalette] {
1197 &[
1198 ColorPalette::Vibrant,
1199 ColorPalette::Realistic,
1200 ColorPalette::HighContrast,
1201 ColorPalette::Monochrome,
1202 ]
1203 }
1204
1205 pub fn name(&self) -> &'static str {
1207 match self {
1208 ColorPalette::Vibrant => "Vibrant",
1209 ColorPalette::Realistic => "Realistic",
1210 ColorPalette::HighContrast => "High Contrast",
1211 ColorPalette::Monochrome => "Monochrome",
1212 }
1213 }
1214}
1215
1216#[cfg(feature = "color-palette")]
1218pub fn get_color_for_palette(entity_type: &str, palette: ColorPalette) -> [f32; 4] {
1219 match palette {
1220 ColorPalette::Vibrant => get_vibrant_color(entity_type),
1221 ColorPalette::Realistic => get_realistic_color(entity_type),
1222 ColorPalette::HighContrast => get_high_contrast_color(entity_type),
1223 ColorPalette::Monochrome => get_monochrome_color(entity_type),
1224 }
1225}
1226
1227pub fn get_default_color(entity_type: &str) -> [f32; 4] {
1229 get_vibrant_color(entity_type)
1230}
1231
1232fn get_vibrant_color(entity_type: &str) -> [f32; 4] {
1234 let upper = entity_type.to_uppercase();
1235
1236 if upper.contains("WALL") {
1237 [0.95, 0.90, 0.80, 1.0] } else if upper.contains("SLAB") {
1239 [0.85, 0.82, 0.78, 1.0] } else if upper.contains("ROOF") {
1241 [0.85, 0.45, 0.35, 1.0] } else if upper.contains("BEAM") || upper.contains("COLUMN") || upper.contains("MEMBER") {
1243 [0.45, 0.55, 0.75, 1.0] } else if upper.contains("DOOR") {
1245 [0.65, 0.40, 0.25, 1.0] } else if upper.contains("WINDOW") || upper.contains("CURTAINWALL") {
1247 [0.4, 0.7, 0.9, 0.4] } else if upper.contains("STAIR") || upper.contains("RAMP") {
1249 [0.75, 0.70, 0.65, 1.0] } else if upper.contains("RAILING") {
1251 [0.30, 0.30, 0.35, 1.0] } else if upper.contains("FURNITURE") || upper.contains("FURNISHING") {
1253 [0.70, 0.50, 0.30, 1.0] } else if upper.contains("SPACE") {
1255 [0.7, 0.85, 0.95, 0.15] } else if upper.contains("PLATE") {
1257 [0.70, 0.72, 0.78, 1.0] } else if upper.contains("COVERING") {
1259 [0.88, 0.85, 0.80, 1.0] } else if upper.contains("FOOTING") || upper.contains("PILE") {
1261 [0.60, 0.58, 0.55, 1.0] } else if upper.contains("PROXY") {
1263 [0.75, 0.60, 0.80, 1.0] } else if upper.contains("LIGHTFIXTURE") {
1265 [1.0, 0.9, 0.3, 1.0] } else if upper.contains("FLOW") || upper.contains("DUCT") || upper.contains("PIPE") {
1267 [0.40, 0.75, 0.50, 1.0] } else if upper.contains("ELECTRIC") || upper.contains("ENERGY") {
1269 [0.90, 0.80, 0.30, 1.0] } else if upper.contains("SANITARY") || upper.contains("FIRE") {
1271 [0.95, 0.95, 0.98, 1.0] } else if upper.contains("SHADING") {
1273 [0.40, 0.45, 0.55, 0.85] } else if upper.contains("TRANSPORT") {
1275 [0.45, 0.45, 0.50, 1.0] } else if upper.contains("GEOGRAPHIC") || upper.contains("VIRTUAL") {
1277 [0.65, 0.85, 0.65, 0.3] } else {
1279 [0.80, 0.78, 0.75, 1.0] }
1281}
1282
1283#[cfg(feature = "color-palette")]
1285fn get_realistic_color(entity_type: &str) -> [f32; 4] {
1286 let upper = entity_type.to_uppercase();
1287
1288 if upper.contains("WALL") {
1289 [0.92, 0.85, 0.75, 1.0] } else if upper.contains("SLAB") {
1291 [0.75, 0.73, 0.70, 1.0] } else if upper.contains("ROOF") {
1293 [0.72, 0.55, 0.45, 1.0] } else if upper.contains("BEAM") || upper.contains("COLUMN") || upper.contains("MEMBER") {
1295 [0.60, 0.65, 0.72, 1.0] } else if upper.contains("DOOR") {
1297 [0.55, 0.35, 0.20, 1.0] } else if upper.contains("WINDOW") || upper.contains("CURTAINWALL") {
1299 [0.5, 0.7, 0.85, 0.35] } else if upper.contains("STAIR") || upper.contains("RAMP") {
1301 [0.65, 0.62, 0.58, 1.0] } else if upper.contains("RAILING") {
1303 [0.35, 0.35, 0.38, 1.0] } else if upper.contains("FURNITURE") || upper.contains("FURNISHING") {
1305 [0.65, 0.45, 0.28, 1.0] } else if upper.contains("SPACE") {
1307 [0.8, 0.85, 0.95, 0.12] } else if upper.contains("PLATE") {
1309 [0.68, 0.70, 0.75, 1.0] } else if upper.contains("COVERING") {
1311 [0.82, 0.80, 0.76, 1.0] } else if upper.contains("FOOTING") || upper.contains("PILE") {
1313 [0.55, 0.53, 0.50, 1.0] } else if upper.contains("PROXY") {
1315 [0.70, 0.65, 0.75, 1.0] } else if upper.contains("LIGHTFIXTURE") {
1317 [0.85, 0.78, 0.30, 1.0] } else if upper.contains("FLOW") || upper.contains("DUCT") || upper.contains("PIPE") {
1319 [0.55, 0.70, 0.58, 1.0] } else if upper.contains("ELECTRIC") || upper.contains("ENERGY") {
1321 [0.75, 0.72, 0.45, 1.0] } else if upper.contains("SANITARY") || upper.contains("FIRE") {
1323 [0.92, 0.92, 0.95, 1.0] } else if upper.contains("SHADING") {
1325 [0.45, 0.48, 0.55, 0.8] } else if upper.contains("TRANSPORT") {
1327 [0.40, 0.40, 0.42, 1.0] } else if upper.contains("GEOGRAPHIC") || upper.contains("VIRTUAL") {
1329 [0.75, 0.85, 0.75, 0.25] } else {
1331 [0.75, 0.72, 0.70, 1.0] }
1333}
1334
1335#[cfg(feature = "color-palette")]
1337fn get_high_contrast_color(entity_type: &str) -> [f32; 4] {
1338 let upper = entity_type.to_uppercase();
1339
1340 if upper.contains("WALL") {
1341 [1.0, 0.95, 0.85, 1.0] } else if upper.contains("SLAB") {
1343 [0.7, 0.7, 0.7, 1.0] } else if upper.contains("ROOF") {
1345 [0.9, 0.3, 0.2, 1.0] } else if upper.contains("BEAM") || upper.contains("COLUMN") || upper.contains("MEMBER") {
1347 [0.2, 0.4, 0.8, 1.0] } else if upper.contains("DOOR") {
1349 [0.6, 0.3, 0.1, 1.0] } else if upper.contains("WINDOW") || upper.contains("CURTAINWALL") {
1351 [0.3, 0.7, 1.0, 0.5] } else if upper.contains("STAIR") || upper.contains("RAMP") {
1353 [0.9, 0.7, 0.5, 1.0] } else if upper.contains("RAILING") {
1355 [0.2, 0.2, 0.2, 1.0] } else if upper.contains("FURNITURE") || upper.contains("FURNISHING") {
1357 [0.8, 0.5, 0.2, 1.0] } else if upper.contains("SPACE") {
1359 [0.6, 0.8, 1.0, 0.2] } else if upper.contains("PLATE") {
1361 [0.5, 0.5, 0.6, 1.0] } else if upper.contains("COVERING") {
1363 [0.95, 0.9, 0.85, 1.0] } else if upper.contains("FOOTING") || upper.contains("PILE") {
1365 [0.4, 0.4, 0.35, 1.0] } else if upper.contains("PROXY") {
1367 [0.8, 0.4, 0.9, 1.0] } else if upper.contains("LIGHTFIXTURE") {
1369 [1.0, 0.95, 0.2, 1.0] } else if upper.contains("FLOW") || upper.contains("DUCT") || upper.contains("PIPE") {
1371 [0.2, 0.9, 0.4, 1.0] } else if upper.contains("ELECTRIC") || upper.contains("ENERGY") {
1373 [1.0, 0.9, 0.2, 1.0] } else if upper.contains("SANITARY") || upper.contains("FIRE") {
1375 [1.0, 1.0, 1.0, 1.0] } else if upper.contains("SHADING") {
1377 [0.3, 0.35, 0.5, 0.9] } else if upper.contains("TRANSPORT") {
1379 [0.3, 0.3, 0.3, 1.0] } else if upper.contains("GEOGRAPHIC") || upper.contains("VIRTUAL") {
1381 [0.5, 1.0, 0.5, 0.35] } else {
1383 [0.85, 0.85, 0.85, 1.0] }
1385}
1386
1387#[cfg(feature = "color-palette")]
1389fn get_monochrome_color(entity_type: &str) -> [f32; 4] {
1390 let upper = entity_type.to_uppercase();
1391
1392 if upper.contains("WALL") {
1394 [0.85, 0.85, 0.85, 1.0] } else if upper.contains("SLAB") {
1396 [0.70, 0.70, 0.70, 1.0] } else if upper.contains("ROOF") {
1398 [0.60, 0.60, 0.60, 1.0] } else if upper.contains("BEAM") || upper.contains("COLUMN") || upper.contains("MEMBER") {
1400 [0.50, 0.50, 0.50, 1.0] } else if upper.contains("DOOR") {
1402 [0.40, 0.40, 0.40, 1.0] } else if upper.contains("WINDOW") || upper.contains("CURTAINWALL") {
1404 [0.75, 0.75, 0.75, 0.4] } else if upper.contains("STAIR") || upper.contains("RAMP") {
1406 [0.65, 0.65, 0.65, 1.0] } else if upper.contains("RAILING") {
1408 [0.30, 0.30, 0.30, 1.0] } else if upper.contains("FURNITURE") || upper.contains("FURNISHING") {
1410 [0.55, 0.55, 0.55, 1.0] } else if upper.contains("SPACE") {
1412 [0.90, 0.90, 0.90, 0.15] } else if upper.contains("PLATE") {
1414 [0.60, 0.60, 0.60, 1.0] } else if upper.contains("COVERING") {
1416 [0.80, 0.80, 0.80, 1.0] } else if upper.contains("FOOTING") || upper.contains("PILE") {
1418 [0.45, 0.45, 0.45, 1.0] } else if upper.contains("PROXY") {
1420 [0.70, 0.70, 0.70, 1.0] } else if upper.contains("LIGHTFIXTURE") {
1422 [0.60, 0.60, 0.60, 1.0] } else if upper.contains("FLOW") || upper.contains("DUCT") || upper.contains("PIPE") {
1424 [0.55, 0.55, 0.55, 1.0] } else if upper.contains("ELECTRIC") || upper.contains("ENERGY") {
1426 [0.65, 0.65, 0.65, 1.0] } else if upper.contains("SANITARY") || upper.contains("FIRE") {
1428 [0.95, 0.95, 0.95, 1.0] } else if upper.contains("SHADING") {
1430 [0.35, 0.35, 0.35, 0.85] } else if upper.contains("TRANSPORT") {
1432 [0.40, 0.40, 0.40, 1.0] } else if upper.contains("GEOGRAPHIC") || upper.contains("VIRTUAL") {
1434 [0.85, 0.85, 0.85, 0.25] } else {
1436 [0.75, 0.75, 0.75, 1.0] }
1438}
1439
1440fn compute_flat_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
1442 let mut normals = vec![[0.0f32, 0.0, 0.0]; positions.len()];
1443
1444 for tri in indices.chunks(3) {
1446 if tri.len() < 3 {
1447 continue;
1448 }
1449 let i0 = tri[0] as usize;
1450 let i1 = tri[1] as usize;
1451 let i2 = tri[2] as usize;
1452
1453 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
1454 continue;
1455 }
1456
1457 let p0 = Vec3::from_array(positions[i0]);
1458 let p1 = Vec3::from_array(positions[i1]);
1459 let p2 = Vec3::from_array(positions[i2]);
1460
1461 let edge1 = p1 - p0;
1462 let edge2 = p2 - p0;
1463 let face_normal = edge1.cross(edge2);
1464
1465 for &idx in &[i0, i1, i2] {
1467 normals[idx][0] += face_normal.x;
1468 normals[idx][1] += face_normal.y;
1469 normals[idx][2] += face_normal.z;
1470 }
1471 }
1472
1473 for normal in &mut normals {
1475 let len = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
1476 if len > 0.0001 {
1477 normal[0] /= len;
1478 normal[1] /= len;
1479 normal[2] /= len;
1480 } else {
1481 *normal = [0.0, 1.0, 0.0];
1483 }
1484 }
1485
1486 normals
1487}