1use crate::PhotometricWeb;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Vertex {
8 pub x: f32,
10 pub y: f32,
12 pub z: f32,
14 pub nx: f32,
16 pub ny: f32,
18 pub nz: f32,
20}
21
22impl Vertex {
23 pub fn new(x: f32, y: f32, z: f32) -> Self {
25 Self {
26 x,
27 y,
28 z,
29 nx: 0.0,
30 ny: 0.0,
31 nz: 0.0,
32 }
33 }
34
35 pub fn with_normal(x: f32, y: f32, z: f32, nx: f32, ny: f32, nz: f32) -> Self {
37 Self {
38 x,
39 y,
40 z,
41 nx,
42 ny,
43 nz,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
53pub struct LdcMesh {
54 pub vertices: Vec<Vertex>,
56 pub indices: Vec<u32>,
58 pub c_divisions: usize,
60 pub g_divisions: usize,
62}
63
64impl LdcMesh {
65 pub fn from_photweb(web: &PhotometricWeb, c_step: f64, g_step: f64, scale: f32) -> Self {
78 let mut vertices = Vec::new();
79 let mut indices = Vec::new();
80
81 let c_count = (360.0 / c_step).ceil() as usize + 1;
83 let g_count = (180.0 / g_step).ceil() as usize + 1;
84
85 for gi in 0..g_count {
87 let g_angle = (gi as f64 * g_step).min(180.0);
88 let g_rad = g_angle.to_radians();
89
90 for ci in 0..c_count {
91 let c_angle = (ci as f64 * c_step).min(360.0);
92 let c_rad = c_angle.to_radians();
93
94 let radius = web.sample_normalized(c_angle, g_angle) as f32 * scale;
96
97 let sin_g = g_rad.sin() as f32;
100 let cos_g = g_rad.cos() as f32;
101 let sin_c = c_rad.sin() as f32;
102 let cos_c = c_rad.cos() as f32;
103
104 let x = radius * sin_g * sin_c;
105 let y = -radius * cos_g; let z = radius * sin_g * cos_c;
107
108 let len = (x * x + y * y + z * z).sqrt();
110 let (nx, ny, nz) = if len > 0.0001 {
111 (x / len, y / len, z / len)
112 } else {
113 (0.0, -1.0, 0.0) };
115
116 vertices.push(Vertex::with_normal(x, y, z, nx, ny, nz));
117 }
118 }
119
120 for gi in 0..g_count - 1 {
123 for ci in 0..c_count - 1 {
124 let i00 = (gi * c_count + ci) as u32;
125 let i01 = (gi * c_count + ci + 1) as u32;
126 let i10 = ((gi + 1) * c_count + ci) as u32;
127 let i11 = ((gi + 1) * c_count + ci + 1) as u32;
128
129 indices.push(i00);
132 indices.push(i10);
133 indices.push(i01);
134
135 indices.push(i01);
137 indices.push(i10);
138 indices.push(i11);
139 }
140 }
141
142 Self {
143 vertices,
144 indices,
145 c_divisions: c_count,
146 g_divisions: g_count,
147 }
148 }
149
150 pub fn positions_flat(&self) -> Vec<f32> {
154 self.vertices.iter().flat_map(|v| [v.x, v.y, v.z]).collect()
155 }
156
157 pub fn normals_flat(&self) -> Vec<f32> {
159 self.vertices
160 .iter()
161 .flat_map(|v| [v.nx, v.ny, v.nz])
162 .collect()
163 }
164
165 pub fn triangle_count(&self) -> usize {
167 self.indices.len() / 3
168 }
169
170 pub fn vertex_count(&self) -> usize {
172 self.vertices.len()
173 }
174}
175
176impl PhotometricWeb {
177 pub fn generate_ldc_mesh(&self, c_step: f64, g_step: f64, scale: f32) -> LdcMesh {
181 LdcMesh::from_photweb(self, c_step, g_step, scale)
182 }
183
184 pub fn generate_ldc_vertices(
188 &self,
189 c_step: f64,
190 g_step: f64,
191 scale: f32,
192 ) -> Vec<(f32, f32, f32)> {
193 let mesh = self.generate_ldc_mesh(c_step, g_step, scale);
194 mesh.vertices.iter().map(|v| (v.x, v.y, v.z)).collect()
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use eulumdat::Symmetry;
202
203 fn create_uniform_web() -> PhotometricWeb {
204 PhotometricWeb::new(
206 vec![0.0, 90.0, 180.0, 270.0],
207 vec![0.0, 45.0, 90.0, 135.0, 180.0],
208 vec![
209 vec![100.0, 100.0, 100.0, 100.0, 100.0],
210 vec![100.0, 100.0, 100.0, 100.0, 100.0],
211 vec![100.0, 100.0, 100.0, 100.0, 100.0],
212 vec![100.0, 100.0, 100.0, 100.0, 100.0],
213 ],
214 Symmetry::None,
215 )
216 }
217
218 #[test]
219 fn test_ldc_mesh_generation() {
220 let web = create_uniform_web();
221 let mesh = web.generate_ldc_mesh(45.0, 45.0, 1.0);
222
223 assert!(mesh.vertex_count() > 0);
225 assert!(mesh.triangle_count() > 0);
226
227 for &idx in &mesh.indices {
229 assert!((idx as usize) < mesh.vertex_count());
230 }
231 }
232
233 #[test]
234 fn test_uniform_sphere_radii() {
235 let web = create_uniform_web();
236 let mesh = web.generate_ldc_mesh(30.0, 30.0, 1.0);
237
238 for v in &mesh.vertices {
240 let r = (v.x * v.x + v.y * v.y + v.z * v.z).sqrt();
241 if r > 0.01 {
243 assert!((r - 1.0).abs() < 0.01, "Expected radius ~1.0, got {}", r);
244 }
245 }
246 }
247
248 #[test]
249 fn test_nadir_zenith_positions() {
250 let web = create_uniform_web();
251 let mesh = web.generate_ldc_mesh(90.0, 90.0, 1.0);
252
253 let nadir = mesh.vertices.iter().find(|v| v.y < -0.9);
255 assert!(nadir.is_some(), "Should have nadir vertex");
256
257 let zenith = mesh.vertices.iter().find(|v| v.y > 0.9);
259 assert!(zenith.is_some(), "Should have zenith vertex");
260 }
261
262 #[test]
263 fn test_flat_arrays() {
264 let web = create_uniform_web();
265 let mesh = web.generate_ldc_mesh(90.0, 90.0, 1.0);
266
267 let positions = mesh.positions_flat();
268 let normals = mesh.normals_flat();
269
270 assert_eq!(positions.len(), mesh.vertex_count() * 3);
271 assert_eq!(normals.len(), mesh.vertex_count() * 3);
272 }
273}