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}