use crate::PhotometricWeb;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorMode {
#[default]
Heatmap,
CPlaneRainbow,
Solid,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub fn from_heatmap(intensity: f32) -> Self {
let v = intensity.clamp(0.0, 1.0);
let (r, g, b) = if v < 0.25 {
let t = v / 0.25;
(0.0, t, 1.0)
} else if v < 0.5 {
let t = (v - 0.25) / 0.25;
(0.0, 1.0, 1.0 - t)
} else if v < 0.75 {
let t = (v - 0.5) / 0.25;
(t, 1.0, 0.0)
} else {
let t = (v - 0.75) / 0.25;
(1.0, 1.0 - t, 0.0)
};
Self::new(r, g, b, 0.9)
}
pub fn from_c_plane_angle(c_angle: f32) -> Self {
let hue = c_angle / 360.0;
let (r, g, b) = hsl_to_rgb(hue, 0.7, 0.5);
Self::new(r, g, b, 0.9)
}
pub fn solid_default() -> Self {
Self::new(0.3, 0.5, 0.9, 0.9)
}
}
pub fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
if s == 0.0 {
return (l, l, l);
}
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
return p + (q - p) * 6.0 * t;
}
if t < 1.0 / 2.0 {
return q;
}
if t < 2.0 / 3.0 {
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
}
p
}
(
hue_to_rgb(p, q, h + 1.0 / 3.0),
hue_to_rgb(p, q, h),
hue_to_rgb(p, q, h - 1.0 / 3.0),
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vertex {
pub x: f32,
pub y: f32,
pub z: f32,
pub nx: f32,
pub ny: f32,
pub nz: f32,
}
impl Vertex {
pub fn new(x: f32, y: f32, z: f32) -> Self {
Self {
x,
y,
z,
nx: 0.0,
ny: 0.0,
nz: 0.0,
}
}
pub fn with_normal(x: f32, y: f32, z: f32, nx: f32, ny: f32, nz: f32) -> Self {
Self {
x,
y,
z,
nx,
ny,
nz,
}
}
}
#[derive(Debug, Clone)]
pub struct LdcMesh {
pub vertices: Vec<Vertex>,
pub indices: Vec<u32>,
pub c_divisions: usize,
pub g_divisions: usize,
}
impl LdcMesh {
pub fn from_photweb(web: &PhotometricWeb, c_step: f64, g_step: f64, scale: f32) -> Self {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let c_count = (360.0 / c_step).ceil() as usize + 1;
let g_count = (180.0 / g_step).ceil() as usize + 1;
for gi in 0..g_count {
let g_angle = (gi as f64 * g_step).min(180.0);
let g_rad = g_angle.to_radians();
for ci in 0..c_count {
let c_angle = (ci as f64 * c_step).min(360.0);
let c_rad = c_angle.to_radians();
let radius = web.sample_normalized(c_angle, g_angle) as f32 * scale;
let sin_g = g_rad.sin() as f32;
let cos_g = g_rad.cos() as f32;
let sin_c = c_rad.sin() as f32;
let cos_c = c_rad.cos() as f32;
let x = radius * sin_g * sin_c;
let y = -radius * cos_g; let z = radius * sin_g * cos_c;
let len = (x * x + y * y + z * z).sqrt();
let (nx, ny, nz) = if len > 0.0001 {
(x / len, y / len, z / len)
} else {
(0.0, -1.0, 0.0) };
vertices.push(Vertex::with_normal(x, y, z, nx, ny, nz));
}
}
for gi in 0..g_count - 1 {
for ci in 0..c_count - 1 {
let i00 = (gi * c_count + ci) as u32;
let i01 = (gi * c_count + ci + 1) as u32;
let i10 = ((gi + 1) * c_count + ci) as u32;
let i11 = ((gi + 1) * c_count + ci + 1) as u32;
indices.push(i00);
indices.push(i10);
indices.push(i01);
indices.push(i01);
indices.push(i10);
indices.push(i11);
}
}
Self {
vertices,
indices,
c_divisions: c_count,
g_divisions: g_count,
}
}
pub fn positions_flat(&self) -> Vec<f32> {
self.vertices.iter().flat_map(|v| [v.x, v.y, v.z]).collect()
}
pub fn normals_flat(&self) -> Vec<f32> {
self.vertices
.iter()
.flat_map(|v| [v.nx, v.ny, v.nz])
.collect()
}
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
pub fn vertex_count(&self) -> usize {
self.vertices.len()
}
pub fn generate_colors(
&self,
web: &PhotometricWeb,
c_step: f64,
g_step: f64,
mode: ColorMode,
) -> Vec<Color> {
let mut colors = Vec::with_capacity(self.vertex_count());
for gi in 0..self.g_divisions {
let g_angle = (gi as f64 * g_step).min(180.0);
for ci in 0..self.c_divisions {
let c_angle = (ci as f64 * c_step).min(360.0);
let color = match mode {
ColorMode::Heatmap => {
let intensity = web.sample_normalized(c_angle, g_angle) as f32;
Color::from_heatmap(intensity)
}
ColorMode::CPlaneRainbow => Color::from_c_plane_angle(c_angle as f32),
ColorMode::Solid => Color::solid_default(),
};
colors.push(color);
}
}
colors
}
pub fn colors_flat(colors: &[Color]) -> Vec<f32> {
colors.iter().flat_map(|c| [c.r, c.g, c.b, c.a]).collect()
}
}
#[derive(Debug, Clone)]
pub struct ColoredLdcMesh {
pub mesh: LdcMesh,
pub colors: Vec<Color>,
pub color_mode: ColorMode,
}
impl ColoredLdcMesh {
pub fn from_photweb(
web: &PhotometricWeb,
c_step: f64,
g_step: f64,
scale: f32,
color_mode: ColorMode,
) -> Self {
let mesh = LdcMesh::from_photweb(web, c_step, g_step, scale);
let colors = mesh.generate_colors(web, c_step, g_step, color_mode);
Self {
mesh,
colors,
color_mode,
}
}
pub fn positions_flat(&self) -> Vec<f32> {
self.mesh.positions_flat()
}
pub fn normals_flat(&self) -> Vec<f32> {
self.mesh.normals_flat()
}
pub fn colors_flat(&self) -> Vec<f32> {
LdcMesh::colors_flat(&self.colors)
}
pub fn indices(&self) -> &[u32] {
&self.mesh.indices
}
pub fn vertex_count(&self) -> usize {
self.mesh.vertex_count()
}
pub fn index_count(&self) -> usize {
self.mesh.indices.len()
}
}
impl PhotometricWeb {
pub fn generate_ldc_mesh(&self, c_step: f64, g_step: f64, scale: f32) -> LdcMesh {
LdcMesh::from_photweb(self, c_step, g_step, scale)
}
pub fn generate_colored_ldc_mesh(
&self,
c_step: f64,
g_step: f64,
scale: f32,
color_mode: ColorMode,
) -> ColoredLdcMesh {
ColoredLdcMesh::from_photweb(self, c_step, g_step, scale, color_mode)
}
pub fn generate_ldc_vertices(
&self,
c_step: f64,
g_step: f64,
scale: f32,
) -> Vec<(f32, f32, f32)> {
let mesh = self.generate_ldc_mesh(c_step, g_step, scale);
mesh.vertices.iter().map(|v| (v.x, v.y, v.z)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use eulumdat::Symmetry;
fn create_uniform_web() -> PhotometricWeb {
PhotometricWeb::new(
vec![0.0, 90.0, 180.0, 270.0],
vec![0.0, 45.0, 90.0, 135.0, 180.0],
vec![
vec![100.0, 100.0, 100.0, 100.0, 100.0],
vec![100.0, 100.0, 100.0, 100.0, 100.0],
vec![100.0, 100.0, 100.0, 100.0, 100.0],
vec![100.0, 100.0, 100.0, 100.0, 100.0],
],
Symmetry::None,
)
}
#[test]
fn test_ldc_mesh_generation() {
let web = create_uniform_web();
let mesh = web.generate_ldc_mesh(45.0, 45.0, 1.0);
assert!(mesh.vertex_count() > 0);
assert!(mesh.triangle_count() > 0);
for &idx in &mesh.indices {
assert!((idx as usize) < mesh.vertex_count());
}
}
#[test]
fn test_uniform_sphere_radii() {
let web = create_uniform_web();
let mesh = web.generate_ldc_mesh(30.0, 30.0, 1.0);
for v in &mesh.vertices {
let r = (v.x * v.x + v.y * v.y + v.z * v.z).sqrt();
if r > 0.01 {
assert!((r - 1.0).abs() < 0.01, "Expected radius ~1.0, got {}", r);
}
}
}
#[test]
fn test_nadir_zenith_positions() {
let web = create_uniform_web();
let mesh = web.generate_ldc_mesh(90.0, 90.0, 1.0);
let nadir = mesh.vertices.iter().find(|v| v.y < -0.9);
assert!(nadir.is_some(), "Should have nadir vertex");
let zenith = mesh.vertices.iter().find(|v| v.y > 0.9);
assert!(zenith.is_some(), "Should have zenith vertex");
}
#[test]
fn test_flat_arrays() {
let web = create_uniform_web();
let mesh = web.generate_ldc_mesh(90.0, 90.0, 1.0);
let positions = mesh.positions_flat();
let normals = mesh.normals_flat();
assert_eq!(positions.len(), mesh.vertex_count() * 3);
assert_eq!(normals.len(), mesh.vertex_count() * 3);
}
#[test]
fn test_colored_mesh() {
let web = create_uniform_web();
let colored = web.generate_colored_ldc_mesh(45.0, 45.0, 1.0, ColorMode::Heatmap);
assert!(colored.vertex_count() > 0);
assert_eq!(colored.colors.len(), colored.vertex_count());
let positions = colored.positions_flat();
let normals = colored.normals_flat();
let colors = colored.colors_flat();
assert_eq!(positions.len(), colored.vertex_count() * 3);
assert_eq!(normals.len(), colored.vertex_count() * 3);
assert_eq!(colors.len(), colored.vertex_count() * 4); }
#[test]
fn test_heatmap_colors() {
let blue = Color::from_heatmap(0.0);
assert!(blue.b > blue.r && blue.b > blue.g);
let red = Color::from_heatmap(1.0);
assert!(red.r > red.g && red.r > red.b);
let mid = Color::from_heatmap(0.5);
assert!(mid.g > 0.5);
}
#[test]
fn test_c_plane_colors() {
let c0 = Color::from_c_plane_angle(0.0);
let c360 = Color::from_c_plane_angle(360.0);
assert!((c0.r - c360.r).abs() < 0.01);
assert!((c0.g - c360.g).abs() < 0.01);
assert!((c0.b - c360.b).abs() < 0.01);
}
}