eulumdat_bevy/
lib.rs

1//! Eulumdat 3D Scene Viewer Library
2//!
3//! This module exposes the Bevy-based 3D viewer as a library that can be
4//! embedded in other applications (like the Leptos WASM editor).
5
6pub mod camera;
7pub mod lighting;
8pub mod scene;
9
10use bevy::prelude::*;
11use camera::CameraPlugin;
12use lighting::PhotometricLightPlugin;
13use scene::{ScenePlugin, SceneType};
14
15/// Resource to track localStorage timestamp for hot-reload
16#[derive(Resource, Default)]
17pub struct LdtTimestamp(pub String);
18
19/// Global scene settings - exposed for external modification
20#[derive(Resource)]
21pub struct SceneSettings {
22    pub scene_type: SceneType,
23    pub room_width: f32,
24    pub room_length: f32,
25    pub room_height: f32,
26    pub mounting_height: f32,
27    pub light_intensity: f32,
28    pub show_luminaire: bool,
29    pub show_photometric_solid: bool,
30    pub show_shadows: bool,
31    pub ldt_data: Option<eulumdat::Eulumdat>,
32}
33
34impl Default for SceneSettings {
35    fn default() -> Self {
36        Self {
37            scene_type: SceneType::Room,
38            room_width: 4.0,
39            room_length: 5.0,
40            room_height: 2.8,
41            mounting_height: 2.5,
42            light_intensity: 1000.0,
43            show_luminaire: true,
44            show_photometric_solid: false,
45            show_shadows: false,
46            ldt_data: None,
47        }
48    }
49}
50
51#[cfg(target_arch = "wasm32")]
52const LDT_STORAGE_KEY: &str = "eulumdat_current_ldt";
53#[cfg(target_arch = "wasm32")]
54const LDT_TIMESTAMP_KEY: &str = "eulumdat_ldt_timestamp";
55
56/// Load LDT from localStorage (WASM only)
57#[cfg(target_arch = "wasm32")]
58pub fn load_from_local_storage() -> Option<eulumdat::Eulumdat> {
59    use wasm_bindgen::JsCast;
60
61    let window = web_sys::window()?;
62    let storage = window.local_storage().ok()??;
63    let ldt_string = storage.get_item(LDT_STORAGE_KEY).ok()??;
64
65    eulumdat::Eulumdat::parse(&ldt_string).ok()
66}
67
68#[cfg(not(target_arch = "wasm32"))]
69pub fn load_from_local_storage() -> Option<eulumdat::Eulumdat> {
70    None
71}
72
73/// Get timestamp from localStorage
74#[cfg(target_arch = "wasm32")]
75pub fn get_ldt_timestamp() -> Option<String> {
76    let window = web_sys::window()?;
77    let storage = window.local_storage().ok()??;
78    storage.get_item(LDT_TIMESTAMP_KEY).ok()?
79}
80
81#[cfg(not(target_arch = "wasm32"))]
82pub fn get_ldt_timestamp() -> Option<String> {
83    None
84}
85
86/// Load default LDT data
87pub fn load_default_ldt() -> Option<eulumdat::Eulumdat> {
88    // For WASM, try to load from localStorage first (synced from editor)
89    #[cfg(target_arch = "wasm32")]
90    {
91        // Try localStorage first
92        if let Some(ldt) = load_from_local_storage() {
93            return Some(ldt);
94        }
95        // Fallback to embedded sample
96        let ldt_content = include_str!("../../eulumdat-wasm/templates/road_luminaire.ldt");
97        eulumdat::Eulumdat::parse(ldt_content).ok()
98    }
99
100    // For native, try to load from file
101    #[cfg(not(target_arch = "wasm32"))]
102    {
103        let sample_paths = [
104            "crates/eulumdat-wasm/templates/road_luminaire.ldt",
105            "../eulumdat-wasm/templates/road_luminaire.ldt",
106            "crates/eulumdat-wasm/templates/fluorescent_luminaire.ldt",
107            "../eulumdat-wasm/templates/fluorescent_luminaire.ldt",
108        ];
109
110        for path in sample_paths {
111            if let Ok(ldt) = eulumdat::Eulumdat::from_file(path) {
112                return Some(ldt);
113            }
114        }
115        None
116    }
117}
118
119/// Poll localStorage for LDT changes
120#[allow(unused_mut, unused_variables)]
121pub fn poll_ldt_changes(
122    mut settings: ResMut<SceneSettings>,
123    mut last_timestamp: ResMut<LdtTimestamp>,
124) {
125    #[cfg(target_arch = "wasm32")]
126    {
127        if let Some(new_timestamp) = get_ldt_timestamp() {
128            if new_timestamp != last_timestamp.0 {
129                // Timestamp changed - reload LDT
130                if let Some(ldt) = load_from_local_storage() {
131                    settings.ldt_data = Some(ldt);
132                    last_timestamp.0 = new_timestamp;
133                }
134            }
135        }
136    }
137}
138
139/// Keyboard control system for the 3D viewer
140pub fn ui_controls_system(
141    mut settings: ResMut<SceneSettings>,
142    keyboard: Res<ButtonInput<KeyCode>>,
143) {
144    // Toggle photometric solid with P key
145    if keyboard.just_pressed(KeyCode::KeyP) {
146        settings.show_photometric_solid = !settings.show_photometric_solid;
147    }
148
149    // Toggle luminaire with L key
150    if keyboard.just_pressed(KeyCode::KeyL) {
151        settings.show_luminaire = !settings.show_luminaire;
152    }
153
154    // Toggle shadows with H key (H = Hide/show shadows)
155    if keyboard.just_pressed(KeyCode::KeyH) {
156        settings.show_shadows = !settings.show_shadows;
157    }
158
159    // Cycle scene types with 1-4 keys
160    if keyboard.just_pressed(KeyCode::Digit1) {
161        settings.scene_type = SceneType::Room;
162    }
163    if keyboard.just_pressed(KeyCode::Digit2) {
164        settings.scene_type = SceneType::Road;
165    }
166    if keyboard.just_pressed(KeyCode::Digit3) {
167        settings.scene_type = SceneType::Parking;
168    }
169    if keyboard.just_pressed(KeyCode::Digit4) {
170        settings.scene_type = SceneType::Outdoor;
171    }
172}
173
174/// Startup system to initialize scene with default LDT
175fn setup_with_default_ldt(mut commands: Commands) {
176    let ldt = load_default_ldt();
177    commands.insert_resource(SceneSettings {
178        ldt_data: ldt,
179        ..default()
180    });
181}
182
183/// Run the 3D viewer on a specific canvas element (WASM)
184///
185/// # Arguments
186/// * `canvas_selector` - CSS selector for the canvas element (e.g., "#bevy-canvas")
187#[cfg(target_arch = "wasm32")]
188pub fn run_on_canvas(canvas_selector: &str) {
189    App::new()
190        .add_plugins(DefaultPlugins.set(WindowPlugin {
191            primary_window: Some(Window {
192                title: "Eulumdat 3D Viewer".to_string(),
193                canvas: Some(canvas_selector.to_string()),
194                fit_canvas_to_parent: true,
195                prevent_default_event_handling: false,
196                ..default()
197            }),
198            ..default()
199        }))
200        .add_plugins((CameraPlugin, ScenePlugin, PhotometricLightPlugin))
201        .insert_resource(SceneSettings::default())
202        .insert_resource(LdtTimestamp::default())
203        .add_systems(Startup, setup_with_default_ldt)
204        .add_systems(Update, ui_controls_system)
205        .add_systems(Update, poll_ldt_changes)
206        .run();
207}
208
209/// Run the 3D viewer in a native window (desktop)
210#[cfg(not(target_arch = "wasm32"))]
211pub fn run_on_canvas(_canvas_selector: &str) {
212    run_native();
213}
214
215/// Run the 3D viewer as a native window (desktop only)
216#[cfg(not(target_arch = "wasm32"))]
217pub fn run_native() {
218    App::new()
219        .add_plugins(DefaultPlugins.set(WindowPlugin {
220            primary_window: Some(Window {
221                title: "Eulumdat 3D Viewer".to_string(),
222                resolution: (1280.0, 720.0).into(),
223                ..default()
224            }),
225            ..default()
226        }))
227        .add_plugins((CameraPlugin, ScenePlugin, PhotometricLightPlugin))
228        .insert_resource(SceneSettings::default())
229        .insert_resource(LdtTimestamp::default())
230        .add_systems(Startup, setup_with_default_ldt)
231        .add_systems(Update, ui_controls_system)
232        .add_systems(Update, poll_ldt_changes)
233        .run();
234}
235
236#[cfg(target_arch = "wasm32")]
237pub fn run_native() {
238    // On WASM, run_native falls back to a default canvas
239    run_on_canvas("#bevy-canvas");
240}