Skip to main content

eulumdat_bevy/viewer/
mod.rs

1//! Viewer module - demo application with scenes, camera, and controls.
2//!
3//! This module provides a complete 3D viewer application built on top
4//! of the generic [`photometric`](crate::photometric) module.
5//!
6//! # Features
7//!
8//! - Pre-built demo scenes (Room, Road, Parking, Outdoor)
9//! - First-person camera controller
10//! - Keyboard controls for toggling visualizations
11//! - Optional localStorage sync for WASM hot-reload
12//!
13//! # Example
14//!
15//! ```ignore
16//! use bevy::prelude::*;
17//! use eulumdat_bevy::viewer::*;
18//!
19//! fn main() {
20//!     App::new()
21//!         .add_plugins(DefaultPlugins)
22//!         .add_plugins(EulumdatViewerPlugin::default())
23//!         .run();
24//! }
25//! ```
26
27pub mod camera;
28pub mod controls;
29#[cfg(feature = "egui-ui")]
30pub mod egui_panel;
31pub mod plugin;
32pub mod scenes;
33pub mod wasm_sync;
34
35pub use camera::{CameraPlugin, FirstPersonCamera};
36pub use controls::{
37    calculate_all_luminaire_transforms, calculate_light_position, LuminaireTransform,
38};
39pub use plugin::EulumdatViewerPlugin;
40pub use scenes::{SceneGeometry, ScenePlugin, SceneType};
41pub use wasm_sync::{
42    load_default_ldt, load_from_local_storage, poll_viewer_settings_changes, LdtTimestamp,
43    ViewerSettingsTimestamp,
44};
45
46use bevy::prelude::*;
47use eulumdat::Eulumdat;
48
49/// Global viewer settings resource.
50///
51/// This resource controls the viewer's behavior and appearance.
52/// Changes to this resource trigger reactive updates to the scene.
53#[derive(Resource, Clone)]
54pub struct ViewerSettings {
55    /// Current scene type
56    pub scene_type: SceneType,
57    /// Room/scene width in meters (X axis)
58    pub room_width: f32,
59    /// Room/scene length in meters (Z axis)
60    pub room_length: f32,
61    /// Room height in meters (Y axis, only for Room scene)
62    pub room_height: f32,
63    /// Luminaire mounting height in meters (for outdoor poles)
64    /// For indoor scenes, this is ignored - use pendulum_length instead
65    pub mounting_height: f32,
66    /// Pendulum/suspension length in meters (for indoor ceiling-mounted luminaires)
67    /// 0.0 = flush mounted to ceiling
68    /// >0.0 = hangs down from ceiling by this amount
69    pub pendulum_length: f32,
70    /// Light intensity (not used directly, available for UI)
71    pub light_intensity: f32,
72    /// Whether to show the luminaire model
73    pub show_luminaire: bool,
74    /// Whether to show the photometric solid
75    pub show_photometric_solid: bool,
76    /// Whether to enable shadows
77    pub show_shadows: bool,
78    /// The LDT data to display
79    pub ldt_data: Option<Eulumdat>,
80    /// Luminaire tilt angle in degrees (for road/outdoor scenes).
81    /// 0 = pointing straight down, 90 = pointing horizontally across the road.
82    /// Default is 15 degrees for road luminaires.
83    pub luminaire_tilt: f32,
84    /// Lane width in meters (for road scenes). Default 3.5m per EN 13201.
85    pub lane_width: f32,
86    /// Number of lanes (for road scenes). Default 2 (one per direction).
87    pub num_lanes: u32,
88    /// Sidewalk width in meters. Default 2.0m.
89    pub sidewalk_width: f32,
90    /// Pole spacing in meters. Calculated based on mounting height if 0.
91    /// Typical: 3-4x mounting height for good uniformity.
92    pub pole_spacing: f32,
93}
94
95impl Default for ViewerSettings {
96    fn default() -> Self {
97        Self {
98            scene_type: SceneType::Room,
99            room_width: 4.0,
100            room_length: 5.0,
101            room_height: 2.8,
102            mounting_height: 8.0, // For outdoor poles (EN 13201: 6-12m typical)
103            pendulum_length: 0.3, // 30cm pendulum for indoor
104            light_intensity: 1000.0,
105            show_luminaire: true,
106            show_photometric_solid: false,
107            show_shadows: false,
108            ldt_data: None,
109            luminaire_tilt: 15.0, // 15 degrees tilt for road luminaires (typical)
110            lane_width: 3.5,      // EN 13201 standard lane width
111            num_lanes: 2,         // Two lanes (one per direction)
112            sidewalk_width: 2.0,  // Standard sidewalk
113            pole_spacing: 0.0,    // 0 = auto-calculate (3.5x mounting height)
114        }
115    }
116}
117
118impl ViewerSettings {
119    /// Calculate effective pole spacing.
120    /// If pole_spacing is 0, use 3.5x mounting height (good uniformity).
121    pub fn effective_pole_spacing(&self) -> f32 {
122        if self.pole_spacing > 0.0 {
123            self.pole_spacing
124        } else {
125            // EN 13201 recommends spacing of 3-4x mounting height
126            self.mounting_height * 3.5
127        }
128    }
129
130    /// Calculate total road width including sidewalks.
131    pub fn total_road_width(&self) -> f32 {
132        self.num_lanes as f32 * self.lane_width + 2.0 * self.sidewalk_width
133    }
134}
135
136impl ViewerSettings {
137    /// Calculate the effective luminaire center height for the current scene.
138    ///
139    /// For Room scenes:
140    /// - Luminaire hangs from ceiling by pendulum_length
141    /// - Center Y = room_height - pendulum_length - half_luminaire_height
142    ///
143    /// For outdoor scenes (Road, Parking, Outdoor):
144    /// - Luminaire is fixed to pole arm at mounting_height
145    /// - Center Y = mounting_height - arm_offset - half_luminaire_height
146    pub fn luminaire_height(&self, ldt: &Eulumdat) -> f32 {
147        let lum_height = (ldt.height / 1000.0).max(0.05) as f32;
148
149        match self.scene_type {
150            SceneType::Room => {
151                // Ceiling mounted with pendulum
152                self.room_height - self.pendulum_length - lum_height / 2.0
153            }
154            SceneType::Road | SceneType::Parking | SceneType::Outdoor => {
155                // Pole mounted - luminaire fixed to arm
156                // Arm is at mounting_height - 0.25, luminaire hangs 0.05m below arm
157                let arm_bottom = self.mounting_height - 0.25;
158                arm_bottom - 0.05 - lum_height / 2.0
159            }
160        }
161    }
162
163    /// Get the attachment point height (where pendulum/cable starts).
164    /// Only meaningful for Room scene.
165    pub fn attachment_height(&self) -> f32 {
166        match self.scene_type {
167            SceneType::Room => self.room_height,
168            _ => self.mounting_height,
169        }
170    }
171}