eulumdat_bevy/photometric/mesh.rs
1//! Mesh generation for photometric visualization.
2//!
3//! This module provides functions to generate:
4//! - Photometric solid meshes (3D representation of light distribution)
5//! - Luminaire geometry meshes (physical shape of the light fixture)
6
7use super::{heatmap_color, PhotometricData};
8use bevy::asset::RenderAssetUsages;
9use bevy::mesh::{Indices, PrimitiveTopology};
10use bevy::prelude::*;
11
12/// Resolution settings for photometric solid mesh generation.
13#[derive(Debug, Clone, Copy, PartialEq, Default)]
14pub enum PhotometricMeshResolution {
15 /// Low resolution: 20° C-step, 10° gamma-step (~324 vertices)
16 Low,
17 /// Medium resolution: 10° C-step, 5° gamma-step (~1296 vertices)
18 #[default]
19 Medium,
20 /// High resolution: 5° C-step, 2.5° gamma-step (~5184 vertices)
21 High,
22 /// Custom resolution with specified step sizes
23 Custom {
24 /// C-plane angle step in degrees
25 c_step: f64,
26 /// Gamma angle step in degrees
27 g_step: f64,
28 },
29}
30
31impl PhotometricMeshResolution {
32 /// Get the step sizes in degrees
33 pub fn steps(&self) -> (f64, f64) {
34 match self {
35 Self::Low => (20.0, 10.0),
36 Self::Medium => (10.0, 5.0),
37 Self::High => (5.0, 2.5),
38 Self::Custom { c_step, g_step } => (*c_step, *g_step),
39 }
40 }
41}
42
43/// Generate a photometric solid mesh from photometric data.
44///
45/// The mesh represents the 3D light distribution as a surface where
46/// the distance from the origin at any direction equals the intensity
47/// in that direction.
48///
49/// # Arguments
50/// * `data` - Photometric data source implementing [`PhotometricData`]
51/// * `resolution` - Mesh resolution (affects vertex count and detail)
52/// * `scale` - Scale factor for the mesh size (default: 0.3)
53///
54/// # Returns
55/// A Bevy Mesh with position, normal, and color attributes
56///
57/// # Example
58/// ```ignore
59/// let mesh = photometric_solid_mesh(&ldt, PhotometricMeshResolution::Medium, 0.3);
60/// commands.spawn(Mesh3dBundle {
61/// mesh: meshes.add(mesh),
62/// material: materials.add(StandardMaterial {
63/// base_color: Color::WHITE,
64/// alpha_mode: AlphaMode::Blend,
65/// ..default()
66/// }),
67/// ..default()
68/// });
69/// ```
70pub fn photometric_solid_mesh<T: PhotometricData>(
71 data: &T,
72 resolution: PhotometricMeshResolution,
73 scale: f32,
74) -> Mesh {
75 let (c_step, g_step) = resolution.steps();
76 let num_c = (360.0 / c_step) as usize;
77 let num_g = (180.0 / g_step) as usize + 1;
78
79 let max_intensity = data.max_intensity();
80 if max_intensity <= 0.0 {
81 // Return empty mesh if no intensity data
82 return Mesh::new(
83 PrimitiveTopology::TriangleList,
84 RenderAssetUsages::default(),
85 );
86 }
87
88 let mut positions = Vec::with_capacity(num_c * num_g);
89 let mut normals = Vec::with_capacity(num_c * num_g);
90 let mut colors = Vec::with_capacity(num_c * num_g);
91 let mut indices = Vec::with_capacity(num_c * (num_g - 1) * 6);
92
93 // Generate vertices
94 for ci in 0..num_c {
95 let c_angle = ci as f64 * c_step;
96 let c_rad = c_angle.to_radians() as f32;
97
98 for gi in 0..num_g {
99 let g_angle = gi as f64 * g_step;
100 let normalized = data.sample(c_angle, g_angle) / max_intensity;
101 let r = normalized as f32 * scale;
102 let g_rad = g_angle.to_radians() as f32;
103
104 // Spherical to Cartesian (Y-down for gamma=0, i.e., nadir)
105 let x = r * g_rad.sin() * c_rad.cos();
106 let z = r * g_rad.sin() * c_rad.sin();
107 let y = -r * g_rad.cos();
108
109 positions.push([x, y, z]);
110
111 // Approximate normals (pointing outward)
112 let len = (x * x + y * y + z * z).sqrt().max(0.001);
113 normals.push([x / len, y / len, z / len]);
114
115 // Heatmap color based on intensity
116 let (cr, cg, cb) = heatmap_color(normalized);
117 colors.push([cr, cg, cb, 0.7]); // Semi-transparent
118 }
119 }
120
121 // Generate triangle indices
122 for c in 0..num_c {
123 let next_c = (c + 1) % num_c;
124 for g in 0..(num_g - 1) {
125 let v0 = (c * num_g + g) as u32;
126 let v1 = (next_c * num_g + g) as u32;
127 let v2 = (next_c * num_g + (g + 1)) as u32;
128 let v3 = (c * num_g + (g + 1)) as u32;
129
130 // Two triangles per quad
131 indices.push(v0);
132 indices.push(v1);
133 indices.push(v2);
134
135 indices.push(v0);
136 indices.push(v2);
137 indices.push(v3);
138 }
139 }
140
141 let mut mesh = Mesh::new(
142 PrimitiveTopology::TriangleList,
143 RenderAssetUsages::default(),
144 );
145 mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
146 mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
147 mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
148 mesh.insert_indices(Indices::U32(indices));
149
150 mesh
151}
152
153/// Generate a luminaire geometry mesh based on dimensions.
154///
155/// Creates either a box or cylinder mesh depending on the luminaire type:
156/// - Cylindrical: width ≈ 0, length = diameter
157/// - Rectangular: width × length × height box
158///
159/// # Arguments
160/// * `data` - Photometric data source implementing [`PhotometricData`]
161///
162/// # Returns
163/// A Bevy Mesh representing the luminaire geometry
164pub fn luminaire_mesh<T: PhotometricData>(data: &T) -> Mesh {
165 let (width, length, height) = data.dimensions();
166
167 if data.is_cylindrical() {
168 // Cylindrical luminaire: length is diameter
169 let radius = length.max(0.1) / 2.0;
170 Cylinder::new(radius, height).into()
171 } else {
172 // Rectangular luminaire
173 Cuboid::new(width.max(0.1), height, length.max(0.1)).into()
174 }
175}
176
177/// Create a material for the luminaire model.
178///
179/// Returns a semi-emissive metallic material that glows with the light color.
180///
181/// # Arguments
182/// * `light_color` - The color of the light
183///
184/// # Returns
185/// StandardMaterial configured for luminaire visualization
186pub fn luminaire_material(light_color: Color) -> StandardMaterial {
187 let linear = light_color.to_linear();
188 StandardMaterial {
189 base_color: Color::srgb(0.3, 0.3, 0.3),
190 emissive: LinearRgba::new(linear.red * 2.0, linear.green * 2.0, linear.blue * 2.0, 1.0),
191 metallic: 0.8,
192 perceptual_roughness: 0.3,
193 ..default()
194 }
195}
196
197/// Create a material for the photometric solid.
198///
199/// Returns a transparent material suitable for the photometric solid mesh.
200///
201/// # Returns
202/// StandardMaterial configured for photometric solid visualization
203pub fn photometric_solid_material() -> StandardMaterial {
204 StandardMaterial {
205 base_color: Color::WHITE,
206 alpha_mode: AlphaMode::Blend,
207 double_sided: true,
208 cull_mode: None,
209 ..default()
210 }
211}