1use serde::Serialize;
10use serde_json::Value;
11
12use crate::{Component, Entity, Instance, JsonError, Resource, Transform, World};
13
14pub const TEXTURE_SWITCH_START: u8 = 1;
16
17#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
27pub struct TextureSwitch {
28 pub border: f32,
30 pub play: f32,
33 pub scale: f32,
35}
36
37impl TextureSwitch {
38 pub fn next(&self, current: u8, distance: f32, state: TextureSwitchState) -> u8 {
41 let threshold = if current == 0 {
42 (self.border + self.play) * self.scale * state.load
43 } else {
44 (self.border - self.play) * self.scale * state.load * state.ramp
45 };
46 u8::from(distance >= threshold)
47 }
48}
49
50fn positive(value: f32, what: &str) -> Result<(), String> {
51 if value.is_finite() && value > 0.0 {
52 Ok(())
53 } else {
54 Err(format!("{what} {value} is not a finite positive number"))
55 }
56}
57
58impl Component for TextureSwitch {
59 const NAME: &'static str = "core.texture_switch";
60 const GEOMETRY: crate::Geometry = crate::Geometry::None;
61 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
63 crate::world::Linearity::Invariant
64 }
65 fn to_json(&self) -> Result<Option<Value>, JsonError> {
66 crate::json::to_value(self).map(Some)
67 }
68 fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
70 if !w.has::<Instance>(e) {
71 return Err("a texture switch on an entity that is not a placement (no core.instance)".into());
72 }
73 positive(self.border, "border")?;
74 positive(self.scale, "scale")?;
75 if !(self.play.is_finite() && self.play >= 0.0) {
76 return Err(format!("play {} is not a finite number of 0 or more", self.play));
77 }
78 Ok(())
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
87pub struct TextureSwitchView {
88 pub reference_fov_y: f32,
90 pub reference_height: u32,
92 pub state: TextureSwitchState,
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
99pub struct TextureSwitchState {
100 pub load: f32,
102 pub ramp: f32,
104}
105
106impl TextureSwitchState {
107 pub const SETTLED: TextureSwitchState = TextureSwitchState { load: 1.0, ramp: 1.0 };
109}
110
111impl TextureSwitchView {
112 pub fn factor(&self, fov_y: f32, height: u32) -> f32 {
116 let fov = (fov_y / 2.0).tan() / (self.reference_fov_y / 2.0).tan();
117 if self.reference_height == 0 {
118 fov
119 } else {
120 fov / (height as f32 / self.reference_height as f32)
121 }
122 }
123}
124
125impl Resource for TextureSwitchView {
126 const NAME: &'static str = "core.texture_switch_view";
127 fn to_json(&self) -> Result<Option<Value>, JsonError> {
128 crate::json::to_value(self).map(Some)
129 }
130 fn verify(&self, _w: &World) -> Result<(), String> {
132 let f = self.reference_fov_y;
133 if !(f.is_finite() && f > 0.0 && f < std::f32::consts::PI) {
134 return Err(format!(
135 "reference vertical field of view {f} is not between 0 and pi radians"
136 ));
137 }
138 for (what, v) in [("load", self.state.load), ("ramp", self.state.ramp)] {
139 if !(0.0..=1.0).contains(&v) {
140 return Err(format!("state {what} {v} is not from 0 to 1"));
141 }
142 }
143 Ok(())
144 }
145}
146
147pub fn world_box(
150 boxes: impl IntoIterator<Item = ([f32; 3], [f32; 3])>,
151 t: &Transform,
152) -> Option<([f32; 3], [f32; 3])> {
153 let mut out: Option<([f32; 3], [f32; 3])> = None;
154 for (lo, hi) in boxes {
155 for corner in 0..8 {
156 let local = [
157 if corner & 1 == 0 { lo[0] } else { hi[0] },
158 if corner & 2 == 0 { lo[1] } else { hi[1] },
159 if corner & 4 == 0 { lo[2] } else { hi[2] },
160 ];
161 let p: [f32; 3] = std::array::from_fn(|k| {
162 t.cols[0][k] * local[0] + t.cols[1][k] * local[1] + t.cols[2][k] * local[2] + t.cols[3][k]
163 });
164 let (min, max) = out.get_or_insert((p, p));
165 for k in 0..3 {
166 min[k] = min[k].min(p[k]);
167 max[k] = max[k].max(p[k]);
168 }
169 }
170 }
171 out
172}
173
174pub fn box_distance(point: [f32; 3], min: [f32; 3], max: [f32; 3]) -> f32 {
176 (0..3)
177 .map(|k| {
178 let d = point[k] - point[k].clamp(min[k], max[k]);
179 d * d
180 })
181 .sum::<f32>()
182 .sqrt()
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 const SWITCH: TextureSwitch = TextureSwitch {
190 border: 30.0,
191 play: 5.0,
192 scale: 1.0,
193 };
194
195 const SETTLED: TextureSwitchState = TextureSwitchState::SETTLED;
196
197 #[test]
199 fn the_level_moves_with_hysteresis() {
200 assert_eq!(SWITCH.next(0, 34.9, SETTLED), 0);
201 assert_eq!(SWITCH.next(0, 35.0, SETTLED), 1);
202 assert_eq!(SWITCH.next(1, 25.0, SETTLED), 1);
203 assert_eq!(SWITCH.next(1, 24.9, SETTLED), 0);
204 for d in [25.0, 30.0, 34.9] {
205 assert_eq!(SWITCH.next(0, d, SETTLED), 0, "{d}");
206 assert_eq!(SWITCH.next(1, d, SETTLED), 1, "{d}");
207 }
208 assert_eq!(SWITCH.next(TEXTURE_SWITCH_START, 30.0, SETTLED), 1);
210 let far = TextureSwitch { scale: 2.0, ..SWITCH };
212 assert_eq!((far.next(0, 69.9, SETTLED), far.next(0, 70.0, SETTLED)), (0, 1));
213 assert_eq!((far.next(1, 50.0, SETTLED), far.next(1, 49.9, SETTLED)), (1, 0));
214 }
215
216 #[test]
218 fn the_state_moves_the_thresholds() {
219 let half = TextureSwitchState { load: 0.5, ramp: 1.0 };
220 assert_eq!((SWITCH.next(0, 17.4, half), SWITCH.next(0, 17.5, half)), (0, 1));
221 assert_eq!((SWITCH.next(1, 12.5, half), SWITCH.next(1, 12.4, half)), (1, 0));
222 let ramping = TextureSwitchState { load: 1.0, ramp: 0.5 };
223 assert_eq!(
224 (SWITCH.next(0, 34.9, ramping), SWITCH.next(0, 35.0, ramping)),
225 (0, 1)
226 );
227 assert_eq!(
228 (SWITCH.next(1, 12.5, ramping), SWITCH.next(1, 12.4, ramping)),
229 (1, 0)
230 );
231 let off = TextureSwitchState { load: 1.0, ramp: 0.0 };
232 assert_eq!(SWITCH.next(1, 0.0, off), 1);
233 }
234
235 #[test]
237 fn the_distance_is_to_the_world_box() {
238 let (lo, hi) = ([-1.0, 0.0, -1.0], [1.0, 2.0, 1.0]);
239 assert_eq!(box_distance([0.0, 1.0, 0.0], lo, hi), 0.0);
240 assert_eq!(box_distance([4.0, 1.0, 0.0], lo, hi), 3.0);
241 assert_eq!(box_distance([4.0, 6.0, 0.0], lo, hi), 5.0);
242 let t = Transform {
244 cols: [
245 [0.0, 0.0, -1.0],
246 [0.0, 1.0, 0.0],
247 [1.0, 0.0, 0.0],
248 [10.0, 0.0, 0.0],
249 ],
250 };
251 let boxes = [
252 ([0.0, 0.0, 0.0], [2.0, 1.0, 1.0]),
253 ([0.0, 0.0, 0.0], [1.0, 3.0, 1.0]),
254 ];
255 assert_eq!(world_box(boxes, &t), Some(([10.0, 0.0, -2.0], [11.0, 3.0, 0.0])));
256 assert_eq!(world_box([], &t), None);
257 }
258
259 #[test]
263 fn a_wider_camera_measures_farther() {
264 let view = TextureSwitchView {
265 reference_fov_y: 43f32.to_radians(),
266 reference_height: 0,
267 state: SETTLED,
268 };
269 assert!((view.factor(43f32.to_radians(), 1440) - 1.0).abs() < 1e-6);
270 assert!((view.factor(48f32.to_radians(), 1440) - 1.130).abs() < 5e-4);
271 assert_eq!(
272 view.factor(48f32.to_radians(), 720),
273 view.factor(48f32.to_radians(), 1440)
274 );
275 let tall = TextureSwitchView {
276 reference_height: 720,
277 ..view
278 };
279 assert!((tall.factor(43f32.to_radians(), 1440) - 0.5).abs() < 1e-6);
280 assert!((tall.factor(43f32.to_radians(), 720) - 1.0).abs() < 1e-6);
281 let w = World::new();
282 assert_eq!(view.verify(&w), Ok(()));
283 for bad in [0.0, -1.0, std::f32::consts::PI, f32::NAN] {
284 let bad_view = TextureSwitchView {
285 reference_fov_y: bad,
286 ..view
287 };
288 assert!(bad_view.verify(&w).is_err(), "{bad}");
289 }
290 for state in [
291 TextureSwitchState { load: 1.5, ramp: 1.0 },
292 TextureSwitchState {
293 load: 1.0,
294 ramp: -0.1,
295 },
296 TextureSwitchState {
297 load: f32::NAN,
298 ramp: 1.0,
299 },
300 ] {
301 assert!(
302 TextureSwitchView { state, ..view }.verify(&w).is_err(),
303 "{state:?}"
304 );
305 }
306 }
307
308 #[test]
310 fn a_switch_is_on_a_placement_with_positive_thresholds() {
311 let mut w = World::new();
312 let prototype = w.spawn();
313 let placed = w.spawn();
314 w.insert(
315 placed,
316 Transform {
317 cols: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0; 3]],
318 },
319 );
320 w.insert(
321 placed,
322 Instance {
323 prototype,
324 variant: 0,
325 },
326 );
327 assert_eq!(SWITCH.verify(placed, &w), Ok(()));
328 assert!(SWITCH.verify(prototype, &w).is_err());
329 for bad in [
330 TextureSwitch {
331 border: 0.0,
332 ..SWITCH
333 },
334 TextureSwitch { scale: 0.0, ..SWITCH },
335 TextureSwitch { play: -1.0, ..SWITCH },
336 TextureSwitch {
337 border: f32::NAN,
338 ..SWITCH
339 },
340 ] {
341 assert!(bad.verify(placed, &w).is_err(), "{bad:?}");
342 }
343 assert_eq!(TextureSwitch { play: 40.0, ..SWITCH }.verify(placed, &w), Ok(()));
345 }
346}