Skip to main content

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}