#![allow(unexpected_cfgs)]
pub mod camera;
pub mod loader;
pub mod mesh;
#[cfg(feature = "photometric")]
pub mod photometric;
pub mod picking;
pub mod section;
pub mod storage;
#[cfg(feature = "bevy-ui")]
pub mod ui;
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub mod native_view;
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub mod ffi;
use bevy::prelude::*;
use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
static DEBUG_MODE: AtomicBool = AtomicBool::new(false);
static PENDING_MESHES: Mutex<Option<Vec<IfcMesh>>> = Mutex::new(None);
pub fn set_pending_meshes(meshes: Vec<IfcMesh>) {
let count = meshes.len();
let mut guard = PENDING_MESHES.lock().unwrap();
*guard = Some(meshes);
log(&format!("[Bevy] Pending meshes set: {} meshes", count));
}
pub fn take_pending_meshes() -> Option<Vec<IfcMesh>> {
let mut guard = PENDING_MESHES.lock().unwrap();
guard.take()
}
pub fn has_pending_meshes() -> bool {
let guard = PENDING_MESHES.lock().unwrap();
guard.is_some()
}
pub fn is_debug() -> bool {
DEBUG_MODE.load(Ordering::Relaxed)
}
#[cfg(target_arch = "wasm32")]
fn init_debug_from_url() {
if let Some(window) = web_sys::window() {
if let Ok(search) = window.location().search() {
let search_str: &str = &search;
if search_str.contains("debug=1") || search_str.contains("debug=true") {
DEBUG_MODE.store(true, Ordering::Relaxed);
web_sys::console::log_1(&"[Bevy] Debug mode enabled".into());
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
fn init_debug_from_url() {
if std::env::var("DEBUG").is_ok() {
DEBUG_MODE.store(true, Ordering::Relaxed);
}
}
pub use camera::{CameraController, CameraMode, CameraPlugin};
pub use loader::{LoadIfcContentEvent, LoadIfcFileEvent, LoaderPlugin, OpenFileDialogRequest};
pub use mesh::{AutoFitState, IfcEntity, IfcMesh, IfcMeshSerialized, MeshGeometry, MeshPlugin};
pub use picking::{PickingPlugin, SelectionState};
pub use section::{SectionPlane, SectionPlanePlugin};
pub use storage::*;
#[cfg(feature = "bevy-ui")]
pub use ui::{IfcUiPlugin, UiState};
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub use native_view::{AppView, AppViewPlugin, AppViews};
pub struct IfcViewerPlugin;
impl Plugin for IfcViewerPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<IfcSceneData>()
.init_resource::<ViewerSettings>()
.init_resource::<IfcTimestamp>()
.add_plugins((
CameraPlugin,
MeshPlugin,
PickingPlugin,
SectionPlanePlugin,
LoaderPlugin,
))
.add_systems(Update, (poll_scene_changes, poll_selection_from_storage));
#[cfg(feature = "bevy-ui")]
app.add_plugins(IfcUiPlugin);
#[cfg(feature = "photometric")]
app.add_plugins(photometric::PhotometricLightingPlugin);
}
}
#[derive(Resource, Default)]
pub struct IfcSceneData {
pub meshes: Vec<IfcMesh>,
pub entities: Vec<EntityInfo>,
pub bounds: Option<SceneBounds>,
pub timestamp: u64,
pub dirty: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EntityInfo {
pub id: u64,
pub entity_type: String,
pub name: Option<String>,
pub storey: Option<String>,
pub storey_elevation: Option<f32>,
}
#[derive(Clone, Debug, Default)]
pub struct SceneBounds {
pub min: Vec3,
pub max: Vec3,
}
impl SceneBounds {
pub fn center(&self) -> Vec3 {
(self.min + self.max) * 0.5
}
pub fn size(&self) -> Vec3 {
self.max - self.min
}
pub fn diagonal(&self) -> f32 {
self.size().length()
}
}
#[derive(Resource)]
pub struct ViewerSettings {
pub theme: Theme,
pub show_grid: bool,
pub show_axes: bool,
pub hidden_entities: FxHashSet<u64>,
pub isolated_entities: Option<FxHashSet<u64>>,
pub storey_filter: Option<String>,
}
impl Default for ViewerSettings {
fn default() -> Self {
Self {
theme: Theme::Dark,
show_grid: true,
show_axes: true,
hidden_entities: FxHashSet::default(),
isolated_entities: None,
storey_filter: None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Theme {
Light,
#[default]
Dark,
}
impl Theme {
pub fn background_color(&self) -> Color {
match self {
Theme::Light => Color::srgb(0.95, 0.95, 0.95),
Theme::Dark => Color::srgb(0.12, 0.12, 0.12),
}
}
pub fn grid_color(&self) -> Color {
match self {
Theme::Light => Color::srgba(0.5, 0.5, 0.5, 0.3),
Theme::Dark => Color::srgba(0.4, 0.4, 0.4, 0.3),
}
}
}
#[derive(Resource, Default)]
pub struct IfcTimestamp(pub String);
#[allow(unused_variables, unused_mut)]
pub fn poll_scene_changes(
mut scene_data: ResMut<IfcSceneData>,
mut settings: ResMut<ViewerSettings>,
mut last_timestamp: ResMut<IfcTimestamp>,
mut auto_fit: ResMut<mesh::AutoFitState>,
) {
if let Some(meshes) = take_pending_meshes() {
log_info(&format!(
"[Bevy] Direct mesh transfer: {} meshes (no deserialization!)",
meshes.len()
));
scene_data.entities = meshes
.iter()
.map(|m| EntityInfo {
id: m.entity_id,
entity_type: m.entity_type.clone(),
name: m.name.clone(),
storey: None,
storey_elevation: None,
})
.collect();
scene_data.meshes = meshes;
scene_data.dirty = true;
auto_fit.has_fit = false;
} else {
#[cfg(target_arch = "wasm32")]
{
if let Some(new_timestamp) = storage::get_timestamp() {
if new_timestamp != last_timestamp.0 {
log(&format!(
"[Bevy] Timestamp changed: {} -> {}",
last_timestamp.0, new_timestamp
));
if let Some(geometry) = storage::load_geometry() {
log(&format!(
"[Bevy] Loaded {} meshes from JS bridge",
geometry.len()
));
scene_data.entities = geometry
.iter()
.map(|m| EntityInfo {
id: m.entity_id,
entity_type: m.entity_type.clone(),
name: m.name.clone(),
storey: None,
storey_elevation: None,
})
.collect();
scene_data.meshes = geometry;
scene_data.dirty = true;
auto_fit.has_fit = false;
}
if let Some(selection) = storage::load_selection() {
}
if let Some(visibility) = storage::load_visibility() {
settings.hidden_entities = visibility.hidden.into_iter().collect();
settings.isolated_entities =
visibility.isolated.map(|v| v.into_iter().collect());
}
last_timestamp.0 = new_timestamp;
}
}
}
}
}
#[allow(unused_variables)]
pub fn poll_selection_from_storage(selection: ResMut<picking::SelectionState>) {
#[cfg(target_arch = "wasm32")]
{
if let Some(stored_selection) = storage::load_selection() {
if let Some(source) = storage::get_selection_source() {
if source == "bevy" {
return;
}
}
let new_selection: FxHashSet<u64> = stored_selection.selected_ids.into_iter().collect();
if selection.selected != new_selection {
selection.selected = new_selection;
}
}
}
}
#[cfg(target_arch = "wasm32")]
pub fn log(msg: &str) {
if is_debug() {
web_sys::console::log_1(&msg.into());
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn log(msg: &str) {
if is_debug() {
println!("{}", msg);
}
}
#[cfg(target_arch = "wasm32")]
pub fn log_info(msg: &str) {
web_sys::console::info_1(&msg.into());
}
#[cfg(not(target_arch = "wasm32"))]
pub fn log_info(msg: &str) {
println!("{}", msg);
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn run_on_canvas(canvas_selector: &str) {
console_error_panic_hook::set_once();
init_debug_from_url();
log_info(&format!(
"[Bevy] Starting unified viewer on canvas: {}",
canvas_selector
));
let scene_data = IfcSceneData::default();
let mut app = App::new();
app.insert_resource(scene_data);
app.insert_resource(ViewerSettings::default());
app.insert_resource(IfcTimestamp::default());
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "BIMIFC Viewer".to_string(),
canvas: Some(canvas_selector.to_string()),
fit_canvas_to_parent: true,
prevent_default_event_handling: false,
..default()
}),
..default()
}));
app.add_plugins(IfcViewerPlugin);
app.run();
}
#[cfg(not(target_arch = "wasm32"))]
pub fn run_on_canvas(_canvas_selector: &str) {
run_native();
}
#[cfg(not(target_arch = "wasm32"))]
pub fn run_native() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "BIMIFC Viewer".to_string(),
resolution: (1280u32, 720u32).into(),
..default()
}),
..default()
}))
.insert_resource(ClearColor(Color::srgb(0.1, 0.1, 0.15)))
.add_plugins(IfcViewerPlugin)
.run();
}
#[cfg(target_arch = "wasm32")]
pub fn run_native() {
run_on_canvas("#bevy-canvas");
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn wasm_start() {
log("[Bevy] wasm_start called");
run_native();
}
#[cfg(target_arch = "wasm32")]
pub fn run_with_data(canvas_selector: &str, scene_data: IfcSceneData) {
console_error_panic_hook::set_once();
init_debug_from_url();
log_info(&format!(
"[Bevy] Starting with data: {} meshes, {} entities",
scene_data.meshes.len(),
scene_data.entities.len()
));
let mut app = App::new();
app.insert_resource(scene_data);
app.insert_resource(ViewerSettings::default());
app.insert_resource(IfcTimestamp::default());
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "BIMIFC Viewer".to_string(),
canvas: Some(canvas_selector.to_string()),
fit_canvas_to_parent: true,
prevent_default_event_handling: false,
..default()
}),
..default()
}));
app.add_plugins(IfcViewerPlugin);
app.run();
}
#[cfg(not(target_arch = "wasm32"))]
pub fn run_with_data(_canvas_selector: &str, _scene_data: IfcSceneData) {
run_native();
}