1use crate::PhotometricWeb;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum ColorMode {
12 #[default]
14 Heatmap,
15 CPlaneRainbow,
17 Solid,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Color {
24 pub r: f32,
25 pub g: f32,
26 pub b: f32,
27 pub a: f32,
28}
29
30impl Color {
31 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
32 Self { r, g, b, a }
33 }
34
35 pub fn from_heatmap(intensity: f32) -> Self {
38 let v = intensity.clamp(0.0, 1.0);
39 let (r, g, b) = if v < 0.25 {
40 let t = v / 0.25;
41 (0.0, t, 1.0)
42 } else if v < 0.5 {
43 let t = (v - 0.25) / 0.25;
44 (0.0, 1.0, 1.0 - t)
45 } else if v < 0.75 {
46 let t = (v - 0.5) / 0.25;
47 (t, 1.0, 0.0)
48 } else {
49 let t = (v - 0.75) / 0.25;
50 (1.0, 1.0 - t, 0.0)
51 };
52 Self::new(r, g, b, 0.9)
53 }
54
55 pub fn from_c_plane_angle(c_angle: f32) -> Self {
57 let hue = c_angle / 360.0;
58 let (r, g, b) = hsl_to_rgb(hue, 0.7, 0.5);
59 Self::new(r, g, b, 0.9)
60 }
61
62 pub fn solid_default() -> Self {
64 Self::new(0.3, 0.5, 0.9, 0.9)
65 }
66}
67
68pub fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
70 if s == 0.0 {
71 return (l, l, l);
72 }
73
74 let q = if l < 0.5 {
75 l * (1.0 + s)
76 } else {
77 l + s - l * s
78 };
79 let p = 2.0 * l - q;
80
81 fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
82 if t < 0.0 {
83 t += 1.0;
84 }
85 if t > 1.0 {
86 t -= 1.0;
87 }
88 if t < 1.0 / 6.0 {
89 return p + (q - p) * 6.0 * t;
90 }
91 if t < 1.0 / 2.0 {
92 return q;
93 }
94 if t < 2.0 / 3.0 {
95 return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
96 }
97 p
98 }
99
100 (
101 hue_to_rgb(p, q, h + 1.0 / 3.0),
102 hue_to_rgb(p, q, h),
103 hue_to_rgb(p, q, h - 1.0 / 3.0),
104 )
105}
106
107#[derive(Debug, Clone, Copy, PartialEq)]
109pub struct Vertex {
110 pub x: f32,
112 pub y: f32,
114 pub z: f32,
116 pub nx: f32,
118 pub ny: f32,
120 pub nz: f32,
122}
123
124impl Vertex {
125 pub fn new(x: f32, y: f32, z: f32) -> Self {
127 Self {
128 x,
129 y,
130 z,
131 nx: 0.0,
132 ny: 0.0,
133 nz: 0.0,
134 }
135 }
136
137 pub fn with_normal(x: f32, y: f32, z: f32, nx: f32, ny: f32, nz: f32) -> Self {
139 Self {
140 x,
141 y,
142 z,
143 nx,
144 ny,
145 nz,
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
155pub struct LdcMesh {
156 pub vertices: Vec<Vertex>,
158 pub indices: Vec<u32>,
160 pub c_divisions: usize,
162 pub g_divisions: usize,
164}
165
166impl LdcMesh {
167 pub fn from_photweb(web: &PhotometricWeb, c_step: f64, g_step: f64, scale: f32) -> Self {
180 let mut vertices = Vec::new();
181 let mut indices = Vec::new();
182
183 let c_count = (360.0 / c_step).ceil() as usize + 1;
185 let g_count = (180.0 / g_step).ceil() as usize + 1;
186
187 for gi in 0..g_count {
189 let g_angle = (gi as f64 * g_step).min(180.0);
190 let g_rad = g_angle.to_radians();
191
192 for ci in 0..c_count {
193 let c_angle = (ci as f64 * c_step).min(360.0);
194 let c_rad = c_angle.to_radians();
195
196 let radius = web.sample_normalized(c_angle, g_angle) as f32 * scale;
198
199 let sin_g = g_rad.sin() as f32;
202 let cos_g = g_rad.cos() as f32;
203 let sin_c = c_rad.sin() as f32;
204 let cos_c = c_rad.cos() as f32;
205
206 let x = radius * sin_g * sin_c;
207 let y = -radius * cos_g; let z = radius * sin_g * cos_c;
209
210 let len = (x * x + y * y + z * z).sqrt();
212 let (nx, ny, nz) = if len > 0.0001 {
213 (x / len, y / len, z / len)
214 } else {
215 (0.0, -1.0, 0.0) };
217
218 vertices.push(Vertex::with_normal(x, y, z, nx, ny, nz));
219 }
220 }
221
222 for gi in 0..g_count - 1 {
225 for ci in 0..c_count - 1 {
226 let i00 = (gi * c_count + ci) as u32;
227 let i01 = (gi * c_count + ci + 1) as u32;
228 let i10 = ((gi + 1) * c_count + ci) as u32;
229 let i11 = ((gi + 1) * c_count + ci + 1) as u32;
230
231 indices.push(i00);
234 indices.push(i10);
235 indices.push(i01);
236
237 indices.push(i01);
239 indices.push(i10);
240 indices.push(i11);
241 }
242 }
243
244 Self {
245 vertices,
246 indices,
247 c_divisions: c_count,
248 g_divisions: g_count,
249 }
250 }
251
252 pub fn positions_flat(&self) -> Vec<f32> {
256 self.vertices.iter().flat_map(|v| [v.x, v.y, v.z]).collect()
257 }
258
259 pub fn normals_flat(&self) -> Vec<f32> {
261 self.vertices
262 .iter()
263 .flat_map(|v| [v.nx, v.ny, v.nz])
264 .collect()
265 }
266
267 pub fn triangle_count(&self) -> usize {
269 self.indices.len() / 3
270 }
271
272 pub fn vertex_count(&self) -> usize {
274 self.vertices.len()
275 }
276
277 pub fn generate_colors(
281 &self,
282 web: &PhotometricWeb,
283 c_step: f64,
284 g_step: f64,
285 mode: ColorMode,
286 ) -> Vec<Color> {
287 let mut colors = Vec::with_capacity(self.vertex_count());
288
289 for gi in 0..self.g_divisions {
290 let g_angle = (gi as f64 * g_step).min(180.0);
291 for ci in 0..self.c_divisions {
292 let c_angle = (ci as f64 * c_step).min(360.0);
293
294 let color = match mode {
295 ColorMode::Heatmap => {
296 let intensity = web.sample_normalized(c_angle, g_angle) as f32;
297 Color::from_heatmap(intensity)
298 }
299 ColorMode::CPlaneRainbow => Color::from_c_plane_angle(c_angle as f32),
300 ColorMode::Solid => Color::solid_default(),
301 };
302 colors.push(color);
303 }
304 }
305 colors
306 }
307
308 pub fn colors_flat(colors: &[Color]) -> Vec<f32> {
310 colors.iter().flat_map(|c| [c.r, c.g, c.b, c.a]).collect()
311 }
312}
313
314#[derive(Debug, Clone)]
318pub struct ColoredLdcMesh {
319 pub mesh: LdcMesh,
321 pub colors: Vec<Color>,
323 pub color_mode: ColorMode,
325}
326
327impl ColoredLdcMesh {
328 pub fn from_photweb(
337 web: &PhotometricWeb,
338 c_step: f64,
339 g_step: f64,
340 scale: f32,
341 color_mode: ColorMode,
342 ) -> Self {
343 let mesh = LdcMesh::from_photweb(web, c_step, g_step, scale);
344 let colors = mesh.generate_colors(web, c_step, g_step, color_mode);
345 Self {
346 mesh,
347 colors,
348 color_mode,
349 }
350 }
351
352 pub fn positions_flat(&self) -> Vec<f32> {
354 self.mesh.positions_flat()
355 }
356
357 pub fn normals_flat(&self) -> Vec<f32> {
359 self.mesh.normals_flat()
360 }
361
362 pub fn colors_flat(&self) -> Vec<f32> {
364 LdcMesh::colors_flat(&self.colors)
365 }
366
367 pub fn indices(&self) -> &[u32] {
369 &self.mesh.indices
370 }
371
372 pub fn vertex_count(&self) -> usize {
374 self.mesh.vertex_count()
375 }
376
377 pub fn index_count(&self) -> usize {
379 self.mesh.indices.len()
380 }
381}
382
383impl PhotometricWeb {
384 pub fn generate_ldc_mesh(&self, c_step: f64, g_step: f64, scale: f32) -> LdcMesh {
388 LdcMesh::from_photweb(self, c_step, g_step, scale)
389 }
390
391 pub fn generate_colored_ldc_mesh(
395 &self,
396 c_step: f64,
397 g_step: f64,
398 scale: f32,
399 color_mode: ColorMode,
400 ) -> ColoredLdcMesh {
401 ColoredLdcMesh::from_photweb(self, c_step, g_step, scale, color_mode)
402 }
403
404 pub fn generate_ldc_vertices(
408 &self,
409 c_step: f64,
410 g_step: f64,
411 scale: f32,
412 ) -> Vec<(f32, f32, f32)> {
413 let mesh = self.generate_ldc_mesh(c_step, g_step, scale);
414 mesh.vertices.iter().map(|v| (v.x, v.y, v.z)).collect()
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use eulumdat::Symmetry;
422
423 fn create_uniform_web() -> PhotometricWeb {
424 PhotometricWeb::new(
426 vec![0.0, 90.0, 180.0, 270.0],
427 vec![0.0, 45.0, 90.0, 135.0, 180.0],
428 vec![
429 vec![100.0, 100.0, 100.0, 100.0, 100.0],
430 vec![100.0, 100.0, 100.0, 100.0, 100.0],
431 vec![100.0, 100.0, 100.0, 100.0, 100.0],
432 vec![100.0, 100.0, 100.0, 100.0, 100.0],
433 ],
434 Symmetry::None,
435 )
436 }
437
438 #[test]
439 fn test_ldc_mesh_generation() {
440 let web = create_uniform_web();
441 let mesh = web.generate_ldc_mesh(45.0, 45.0, 1.0);
442
443 assert!(mesh.vertex_count() > 0);
445 assert!(mesh.triangle_count() > 0);
446
447 for &idx in &mesh.indices {
449 assert!((idx as usize) < mesh.vertex_count());
450 }
451 }
452
453 #[test]
454 fn test_uniform_sphere_radii() {
455 let web = create_uniform_web();
456 let mesh = web.generate_ldc_mesh(30.0, 30.0, 1.0);
457
458 for v in &mesh.vertices {
460 let r = (v.x * v.x + v.y * v.y + v.z * v.z).sqrt();
461 if r > 0.01 {
463 assert!((r - 1.0).abs() < 0.01, "Expected radius ~1.0, got {}", r);
464 }
465 }
466 }
467
468 #[test]
469 fn test_nadir_zenith_positions() {
470 let web = create_uniform_web();
471 let mesh = web.generate_ldc_mesh(90.0, 90.0, 1.0);
472
473 let nadir = mesh.vertices.iter().find(|v| v.y < -0.9);
475 assert!(nadir.is_some(), "Should have nadir vertex");
476
477 let zenith = mesh.vertices.iter().find(|v| v.y > 0.9);
479 assert!(zenith.is_some(), "Should have zenith vertex");
480 }
481
482 #[test]
483 fn test_flat_arrays() {
484 let web = create_uniform_web();
485 let mesh = web.generate_ldc_mesh(90.0, 90.0, 1.0);
486
487 let positions = mesh.positions_flat();
488 let normals = mesh.normals_flat();
489
490 assert_eq!(positions.len(), mesh.vertex_count() * 3);
491 assert_eq!(normals.len(), mesh.vertex_count() * 3);
492 }
493
494 #[test]
495 fn test_colored_mesh() {
496 let web = create_uniform_web();
497 let colored = web.generate_colored_ldc_mesh(45.0, 45.0, 1.0, ColorMode::Heatmap);
498
499 assert!(colored.vertex_count() > 0);
501 assert_eq!(colored.colors.len(), colored.vertex_count());
502
503 let positions = colored.positions_flat();
505 let normals = colored.normals_flat();
506 let colors = colored.colors_flat();
507
508 assert_eq!(positions.len(), colored.vertex_count() * 3);
509 assert_eq!(normals.len(), colored.vertex_count() * 3);
510 assert_eq!(colors.len(), colored.vertex_count() * 4); }
512
513 #[test]
514 fn test_heatmap_colors() {
515 let blue = Color::from_heatmap(0.0);
517 assert!(blue.b > blue.r && blue.b > blue.g);
518
519 let red = Color::from_heatmap(1.0);
521 assert!(red.r > red.g && red.r > red.b);
522
523 let mid = Color::from_heatmap(0.5);
525 assert!(mid.g > 0.5);
526 }
527
528 #[test]
529 fn test_c_plane_colors() {
530 let c0 = Color::from_c_plane_angle(0.0);
532 let c360 = Color::from_c_plane_angle(360.0);
533 assert!((c0.r - c360.r).abs() < 0.01);
534 assert!((c0.g - c360.g).abs() < 0.01);
535 assert!((c0.b - c360.b).abs() < 0.01);
536 }
537}