Skip to main content

eulumdat_bevy/viewer/
plugin.rs

1//! EulumdatViewerPlugin - Full demo application plugin.
2//!
3//! This plugin provides a complete 3D viewer with:
4//! - Demo scenes (Room, Road, Parking, Outdoor)
5//! - First-person camera
6//! - Keyboard controls
7//! - Optional localStorage sync for WASM
8
9use super::camera::CameraPlugin;
10use super::controls::{
11    calculate_all_luminaire_transforms, sync_viewer_to_lights, viewer_controls_system,
12};
13use super::scenes::ScenePlugin;
14use super::wasm_sync::{load_default_ldt, LdtTimestamp, ViewerSettingsTimestamp};
15use super::ViewerSettings;
16use crate::eulumdat_impl::EulumdatLightBundle;
17use crate::photometric::PhotometricPlugin;
18use bevy::prelude::*;
19use eulumdat::Eulumdat;
20
21/// Full demo application plugin for the Eulumdat 3D viewer.
22///
23/// This plugin includes:
24/// - [`PhotometricPlugin`] for photometric lighting
25/// - [`CameraPlugin`] for first-person camera
26/// - [`ScenePlugin`] for demo scene geometry
27/// - Keyboard controls (P/L/H/1-4)
28/// - Optional localStorage sync for WASM hot-reload
29///
30/// # Example
31///
32/// ```ignore
33/// use bevy::prelude::*;
34/// use eulumdat_bevy::viewer::*;
35///
36/// fn main() {
37///     App::new()
38///         .add_plugins(DefaultPlugins)
39///         .add_plugins(EulumdatViewerPlugin::default())
40///         .run();
41/// }
42/// ```
43pub struct EulumdatViewerPlugin {
44    /// Initial LDT data to display (optional)
45    pub initial_ldt: Option<Eulumdat>,
46    /// Enable keyboard controls (P, L, H, 1-4 keys). Default: true
47    pub enable_keyboard_controls: bool,
48    /// Enable localStorage polling for hot-reload (WASM only, requires `wasm-sync` feature).
49    /// Default: true when `wasm-sync` feature is enabled, false otherwise.
50    pub enable_local_storage_sync: bool,
51}
52
53impl Default for EulumdatViewerPlugin {
54    fn default() -> Self {
55        Self {
56            initial_ldt: None,
57            enable_keyboard_controls: true,
58            enable_local_storage_sync: cfg!(feature = "wasm-sync"),
59        }
60    }
61}
62
63impl EulumdatViewerPlugin {
64    /// Create a new plugin with default settings.
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Create a plugin with initial LDT data.
70    pub fn with_ldt(ldt: Eulumdat) -> Self {
71        Self {
72            initial_ldt: Some(ldt),
73            enable_keyboard_controls: true,
74            enable_local_storage_sync: cfg!(feature = "wasm-sync"),
75        }
76    }
77}
78
79impl Plugin for EulumdatViewerPlugin {
80    fn build(&self, app: &mut App) {
81        // Add the generic photometric plugin for Eulumdat
82        app.add_plugins(PhotometricPlugin::<Eulumdat>::new());
83
84        // Add viewer-specific plugins
85        app.add_plugins((CameraPlugin, ScenePlugin));
86
87        // Insert viewer settings
88        let settings = ViewerSettings {
89            ldt_data: self.initial_ldt.clone(),
90            ..default()
91        };
92        app.insert_resource(settings);
93        app.insert_resource(LdtTimestamp::default());
94        app.insert_resource(ViewerSettingsTimestamp::default());
95
96        // Add startup system to spawn the light
97        app.add_systems(Startup, setup_viewer_light);
98
99        // Add keyboard controls if enabled
100        if self.enable_keyboard_controls {
101            app.add_systems(Update, viewer_controls_system);
102        }
103
104        // Add localStorage polling if feature is enabled
105        // sync_ldt_to_light runs first and its commands are applied before
106        // sync_viewer_to_lights, preventing stale entity references.
107        #[cfg(feature = "wasm-sync")]
108        if self.enable_local_storage_sync {
109            app.add_systems(
110                Update,
111                (
112                    super::wasm_sync::poll_ldt_changes,
113                    super::wasm_sync::poll_viewer_settings_changes,
114                    sync_ldt_to_light,
115                    ApplyDeferred,
116                    sync_viewer_to_lights,
117                )
118                    .chain(),
119            );
120        }
121
122        // Add sync system for non-wasm builds (no ordering conflict)
123        #[cfg(not(feature = "wasm-sync"))]
124        app.add_systems(Update, sync_viewer_to_lights);
125
126        // Add egui settings panel for native builds only (not WASM - causes font init panic)
127        #[cfg(all(feature = "egui-ui", not(target_arch = "wasm32")))]
128        {
129            app.add_plugins(super::egui_panel::EguiSettingsPlugin);
130        }
131    }
132}
133
134/// Startup system to spawn the initial photometric lights.
135fn setup_viewer_light(mut commands: Commands, settings: Res<ViewerSettings>) {
136    // Try to get LDT data from settings or load default
137    let ldt = settings.ldt_data.clone().or_else(load_default_ldt);
138
139    if let Some(ldt_data) = ldt {
140        // Calculate all luminaire positions and rotations
141        let transforms = calculate_all_luminaire_transforms(&settings, &ldt_data);
142
143        for transform in transforms {
144            commands.spawn(
145                EulumdatLightBundle::new(ldt_data.clone())
146                    .with_transform(
147                        Transform::from_translation(transform.position)
148                            .with_rotation(transform.rotation),
149                    )
150                    .with_solid(settings.show_photometric_solid)
151                    .with_model(settings.show_luminaire)
152                    .with_shadows(settings.show_shadows),
153            );
154        }
155    }
156}
157
158/// System to sync LDT data changes to the light entities.
159/// Despawns existing lights and respawns with new configuration.
160#[cfg(feature = "wasm-sync")]
161fn sync_ldt_to_light(
162    mut commands: Commands,
163    settings: Res<ViewerSettings>,
164    lights: Query<Entity, With<crate::photometric::PhotometricLight<Eulumdat>>>,
165) {
166    if !settings.is_changed() {
167        return;
168    }
169
170    if let Some(ref new_ldt) = settings.ldt_data {
171        // Despawn all existing lights
172        for entity in lights.iter() {
173            commands.entity(entity).despawn();
174        }
175
176        // Spawn new lights with updated configuration
177        let transforms = calculate_all_luminaire_transforms(&settings, new_ldt);
178
179        #[cfg(target_arch = "wasm32")]
180        web_sys::console::log_1(&format!("[Bevy] Spawning {} luminaires", transforms.len()).into());
181
182        for transform in transforms {
183            commands.spawn(
184                EulumdatLightBundle::new(new_ldt.clone())
185                    .with_transform(
186                        Transform::from_translation(transform.position)
187                            .with_rotation(transform.rotation),
188                    )
189                    .with_solid(settings.show_photometric_solid)
190                    .with_model(settings.show_luminaire)
191                    .with_shadows(settings.show_shadows),
192            );
193        }
194    }
195}