concinnity_core/components/
geometry.rs1use crate::components::{GlassPanel, InstancedProp, RectAreaLight, SpotLight};
10use crate::math::{cos, sqrt};
11
12pub const SPOT_MAX_ANGLE_DEG: f32 = 89.9;
15
16fn normalize_or(v: [f32; 3], fallback: [f32; 3]) -> [f32; 3] {
20 let len = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
21 if len < 1e-6 {
22 fallback
23 } else {
24 [v[0] / len, v[1] / len, v[2] / len]
25 }
26}
27
28pub trait InstancedPropGeometry {
30 fn instance_model_matrix(&self, idx: usize) -> Option<[[f32; 4]; 4]>;
33}
34
35impl InstancedPropGeometry for InstancedProp {
36 fn instance_model_matrix(&self, idx: usize) -> Option<[[f32; 4]; 4]> {
39 let xform = self.instances.get(idx)?;
40 Some(crate::gfx::transform::trs_matrix(
41 xform.position,
42 xform.rotation_deg,
43 xform.scale,
44 ))
45 }
46}
47
48pub trait SpotLightGeometry {
50 fn unit_direction(&self) -> [f32; 3];
52 fn cos_inner(&self) -> f32;
55 fn cos_outer(&self) -> f32;
57}
58
59impl SpotLightGeometry for SpotLight {
60 fn unit_direction(&self) -> [f32; 3] {
63 normalize_or(self.direction, [0.0, -1.0, 0.0])
64 }
65
66 fn cos_inner(&self) -> f32 {
68 cos(self.inner_angle.clamp(0.0, self.outer_angle).to_radians())
69 }
70
71 fn cos_outer(&self) -> f32 {
73 cos(self.outer_angle.clamp(0.0, SPOT_MAX_ANGLE_DEG).to_radians())
74 }
75}
76
77pub trait GlassPanelGeometry {
79 fn unit_normal(&self) -> [f32; 3];
81}
82
83impl GlassPanelGeometry for GlassPanel {
84 fn unit_normal(&self) -> [f32; 3] {
88 normalize_or(self.normal, [0.0, 0.0, 1.0])
89 }
90}
91
92pub trait RectAreaLightGeometry {
94 fn unit_normal(&self) -> [f32; 3];
96}
97
98impl RectAreaLightGeometry for RectAreaLight {
99 fn unit_normal(&self) -> [f32; 3] {
102 normalize_or(self.normal, [0.0, -1.0, 0.0])
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn spot(direction: [f32; 3], inner: f32, outer: f32) -> SpotLight {
111 SpotLight {
112 direction,
113 inner_angle: inner,
114 outer_angle: outer,
115 ..SpotLight::default()
116 }
117 }
118
119 #[test]
120 fn spot_direction_normalises() {
121 let d = spot([0.0, -4.0, 0.0], 10.0, 20.0).unit_direction();
122 assert_eq!(d, [0.0, -1.0, 0.0]);
123 }
124
125 #[test]
126 fn degenerate_spot_direction_falls_back_to_down() {
127 assert_eq!(
128 spot([0.0; 3], 10.0, 20.0).unit_direction(),
129 [0.0, -1.0, 0.0]
130 );
131 }
132
133 #[test]
134 fn rect_normal_normalises_and_falls_back_to_down() {
135 let lit = RectAreaLight {
136 normal: [0.0, 0.0, 3.0],
137 ..RectAreaLight::default()
138 };
139 assert_eq!(lit.unit_normal(), [0.0, 0.0, 1.0]);
140 let degenerate = RectAreaLight {
141 normal: [0.0; 3],
142 ..RectAreaLight::default()
143 };
144 assert_eq!(degenerate.unit_normal(), [0.0, -1.0, 0.0]);
145 }
146
147 #[test]
150 fn spot_inner_cosine_never_falls_below_the_outer() {
151 for (inner, outer) in [(10.0, 20.0), (45.0, 20.0), (0.0, 0.0), (-5.0, 30.0)] {
152 let s = spot([0.0, -1.0, 0.0], inner, outer);
153 assert!(
154 s.cos_inner() >= s.cos_outer() - 1e-6,
155 "inner {inner} outer {outer}"
156 );
157 }
158 }
159
160 #[test]
161 fn spot_cosines_match_the_authored_angles() {
162 let s = spot([0.0, -1.0, 0.0], 15.0, 30.0);
163 assert!((s.cos_inner() - 15.0f32.to_radians().cos()).abs() < 1e-6);
164 assert!((s.cos_outer() - 30.0f32.to_radians().cos()).abs() < 1e-6);
165 }
166
167 #[test]
169 fn spot_outer_angle_capped() {
170 let s = spot([0.0, -1.0, 0.0], 0.0, 180.0);
171 assert!((s.cos_outer() - SPOT_MAX_ANGLE_DEG.to_radians().cos()).abs() < 1e-6);
172 }
173}