1use serde::Serialize;
6use serde_json::{Value, json};
7
8use crate::{Component, Entity, JsonError, Surfaces, World};
9
10#[derive(Clone, Debug, PartialEq)]
15pub struct Collider {
16 pub shape: Shape,
17 pub surface: u16,
19}
20
21#[derive(Clone, Debug, PartialEq, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum Shape {
25 Box {
27 center: [f32; 3],
28 half_extents: [f32; 3],
29 },
30 Sphere {
31 center: [f32; 3],
32 radius: f32,
33 },
34 Capsule {
36 a: [f32; 3],
37 b: [f32; 3],
38 radius: f32,
39 },
40 Cylinder {
42 a: [f32; 3],
43 b: [f32; 3],
44 radius: f32,
45 },
46 Convex {
48 points: Vec<[f32; 3]>,
49 },
50 Heightfield,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
56#[serde(rename_all = "snake_case")]
57pub enum Diagonal {
58 Main,
60 Anti,
62}
63
64#[derive(Clone, Debug, PartialEq, Serialize)]
69pub struct Heightfield {
70 pub columns: u32,
71 pub rows: u32,
72 pub spacing: [f32; 2],
73 pub heights: Vec<f32>,
75 pub diagonal: Diagonal,
76 pub holes: Option<Vec<bool>>,
78 pub surfaces: Option<Vec<u16>>,
80}
81
82impl Heightfield {
83 pub fn cells(&self) -> usize {
85 (self.columns.saturating_sub(1) as usize) * (self.rows.saturating_sub(1) as usize)
86 }
87}
88
89fn finite(values: impl IntoIterator<Item = f32>, what: &str) -> Result<(), String> {
90 match values.into_iter().find(|v| !v.is_finite()) {
91 Some(v) => Err(format!("{what}: {v} is not finite")),
92 None => Ok(()),
93 }
94}
95
96fn positive(v: f32, what: &str) -> Result<(), String> {
97 if v.is_finite() && v > 0.0 {
98 Ok(())
99 } else {
100 Err(format!("{what} {v} is not positive"))
101 }
102}
103
104fn surface_known(w: &World, s: u16) -> Result<(), String> {
105 let known = w.resource::<Surfaces>().map_or(0, |t| t.0.len());
106 if usize::from(s) < known {
107 Ok(())
108 } else {
109 Err(format!("surface {s} past the world's {known} surfaces"))
110 }
111}
112
113impl Component for Collider {
114 const NAME: &'static str = "core.collider";
115 const GEOMETRY: crate::Geometry = crate::Geometry::None;
116 fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
121 use crate::world::Linearity;
122 let scaled = |r: f32, s: f64| (f64::from(r) * s) as f32;
123 let shape = match &self.shape {
124 Shape::Convex { points } => Shape::Convex {
125 points: points.iter().map(|&p| l.point(p)).collect(),
126 },
127 Shape::Box { center, half_extents } => {
128 let corners: Vec<[f32; 3]> = (0..8)
129 .map(|k| {
130 let sign = |bit: usize| if k & (1 << bit) == 0 { -1.0 } else { 1.0 };
131 l.point(std::array::from_fn(|i| center[i] + sign(i) * half_extents[i]))
132 })
133 .collect();
134 if l.axis_map().is_some() {
135 let lo: [f32; 3] =
136 std::array::from_fn(|i| corners.iter().map(|c| c[i]).fold(f32::INFINITY, f32::min));
137 let hi: [f32; 3] = std::array::from_fn(|i| {
138 corners.iter().map(|c| c[i]).fold(f32::NEG_INFINITY, f32::max)
139 });
140 Shape::Box {
141 center: l.point(*center),
142 half_extents: std::array::from_fn(|i| (hi[i] - lo[i]) / 2.0),
143 }
144 } else {
145 Shape::Convex { points: corners }
146 }
147 }
148 Shape::Sphere { center, radius } => match l.similarity() {
149 Some(s) => Shape::Sphere {
150 center: l.point(*center),
151 radius: scaled(*radius, s),
152 },
153 None => return Linearity::Unsupported,
154 },
155 Shape::Capsule { a, b, radius } => match l.similarity() {
156 Some(s) => Shape::Capsule {
157 a: l.point(*a),
158 b: l.point(*b),
159 radius: scaled(*radius, s),
160 },
161 None => return Linearity::Unsupported,
162 },
163 Shape::Cylinder { a, b, radius } => match l.similarity() {
164 Some(s) => Shape::Cylinder {
165 a: l.point(*a),
166 b: l.point(*b),
167 radius: scaled(*radius, s),
168 },
169 None => return Linearity::Unsupported,
170 },
171 Shape::Heightfield => return Linearity::Invariant,
172 };
173 Linearity::Applied(Collider {
174 shape,
175 surface: self.surface,
176 })
177 }
178 fn to_json(&self) -> Result<Option<Value>, JsonError> {
180 let shape = match &self.shape {
181 Shape::Convex { points } => json!({"convex": {"points": points.len()}}),
182 other => crate::json::to_value(other)?,
183 };
184 Ok(Some(json!({"shape": shape, "surface": self.surface})))
185 }
186 fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
189 surface_known(w, self.surface)?;
190 match &self.shape {
191 Shape::Box { center, half_extents } => {
192 finite(*center, "box centre")?;
193 half_extents
194 .iter()
195 .try_for_each(|&h| positive(h, "box half extent"))
196 }
197 Shape::Sphere { center, radius } => {
198 finite(*center, "sphere centre")?;
199 positive(*radius, "sphere radius")
200 }
201 Shape::Capsule { a, b, radius } | Shape::Cylinder { a, b, radius } => {
202 finite(a.iter().chain(b).copied(), "axis ends")?;
203 positive(*radius, "radius")
204 }
205 Shape::Convex { points } => {
206 if points.is_empty() {
207 return Err("a convex shape without points".into());
208 }
209 finite(points.iter().flatten().copied(), "convex points")
210 }
211 Shape::Heightfield => {
212 if w.has::<Heightfield>(e) {
213 Ok(())
214 } else {
215 Err("a heightfield shape without the entity's core.heightfield".into())
216 }
217 }
218 }
219 }
220}
221
222impl Component for Heightfield {
223 const NAME: &'static str = "core.heightfield";
224 const GEOMETRY: crate::Geometry = crate::Geometry::None;
225 fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
229 use crate::world::Linearity;
230 let col = |k: usize| l.point(std::array::from_fn(|i| if i == k { 1.0 } else { 0.0 }));
231 let (x, y, z) = (col(0), col(1), col(2));
232 let diagonal = x[1] == 0.0 && x[2] == 0.0 && y[0] == 0.0 && y[2] == 0.0 && z[0] == 0.0 && z[1] == 0.0;
233 if !(diagonal && x[0] > 0.0 && y[1] > 0.0 && z[2] > 0.0) {
234 return Linearity::Unsupported;
235 }
236 let scale = |v: f32, s: f32| (f64::from(v) * f64::from(s)) as f32;
237 let mut h = self.clone();
238 h.spacing = [scale(h.spacing[0], x[0]), scale(h.spacing[1], z[2])];
239 h.heights.iter_mut().for_each(|v| *v = scale(*v, y[1]));
240 Linearity::Applied(h)
241 }
242 fn to_json(&self) -> Result<Option<Value>, JsonError> {
244 Ok(Some(json!({
245 "columns": self.columns,
246 "rows": self.rows,
247 "spacing": crate::json::to_value(&self.spacing)?,
248 "heights": self.heights.len(),
249 "diagonal": crate::json::to_value(&self.diagonal)?,
250 "holes": self.holes.as_ref().map(|h| h.iter().filter(|&&x| x).count()),
251 "surfaces": self.surfaces.as_ref().map(Vec::len),
252 })))
253 }
254 fn verify(&self, _e: Entity, w: &World) -> Result<(), String> {
257 if self.columns < 2 || self.rows < 2 {
258 return Err(format!(
259 "{} x {} samples: a heightfield has at least 2 x 2",
260 self.columns, self.rows
261 ));
262 }
263 self.spacing.iter().try_for_each(|&s| positive(s, "spacing"))?;
264 let samples = self.columns as usize * self.rows as usize;
265 if self.heights.len() != samples {
266 return Err(format!("{} heights for {samples} samples", self.heights.len()));
267 }
268 finite(self.heights.iter().copied(), "heights")?;
269 let cells = self.cells();
270 if let Some(h) = &self.holes
271 && h.len() != cells
272 {
273 return Err(format!("{} holes for {cells} cells", h.len()));
274 }
275 if let Some(s) = &self.surfaces {
276 if s.len() != cells {
277 return Err(format!("{} surfaces for {cells} cells", s.len()));
278 }
279 s.iter().try_for_each(|&s| surface_known(w, s))?;
280 }
281 Ok(())
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 fn world() -> World {
290 let mut w = World::new();
291 let mut s = Surfaces::default();
292 s.index("concrete").unwrap();
293 w.insert_resource(s);
294 w
295 }
296
297 fn field(columns: u32, rows: u32) -> Heightfield {
298 Heightfield {
299 columns,
300 rows,
301 spacing: [1.0, 2.0],
302 heights: vec![0.0; (columns * rows) as usize],
303 diagonal: Diagonal::Main,
304 holes: None,
305 surfaces: None,
306 }
307 }
308
309 #[test]
310 fn shapes_verify_their_sizes_surfaces_and_heightfield() {
311 let mut w = world();
312 let ok = [
313 Shape::Box {
314 center: [0.0; 3],
315 half_extents: [1.0, 2.0, 3.0],
316 },
317 Shape::Sphere {
318 center: [0.0; 3],
319 radius: 0.5,
320 },
321 Shape::Capsule {
322 a: [0.0; 3],
323 b: [0.0, 2.0, 0.0],
324 radius: 0.5,
325 },
326 Shape::Convex {
327 points: vec![[0.0; 3]],
328 },
329 ];
330 for shape in ok {
331 let e = w.spawn();
332 w.insert(e, Collider { shape, surface: 0 });
333 }
334 let terrain = w.spawn();
335 w.insert(terrain, field(3, 2));
336 w.insert(
337 terrain,
338 Collider {
339 shape: Shape::Heightfield,
340 surface: 0,
341 },
342 );
343 assert!(w.verify_components().is_empty(), "{:?}", w.verify_components());
344 let bad = [
345 (
346 Shape::Sphere {
347 center: [0.0; 3],
348 radius: 0.0,
349 },
350 0,
351 "sphere radius 0 is not positive",
352 ),
353 (
354 Shape::Box {
355 center: [f32::NAN, 0.0, 0.0],
356 half_extents: [1.0; 3],
357 },
358 0,
359 "box centre: NaN is not finite",
360 ),
361 (
362 Shape::Convex { points: Vec::new() },
363 0,
364 "a convex shape without points",
365 ),
366 (
367 Shape::Heightfield,
368 0,
369 "a heightfield shape without the entity's core.heightfield",
370 ),
371 (
372 Shape::Sphere {
373 center: [0.0; 3],
374 radius: 1.0,
375 },
376 1,
377 "surface 1 past the world's 1 surfaces",
378 ),
379 ];
380 for (shape, surface, message) in bad {
381 let mut w = world();
382 let e = w.spawn();
383 w.insert(e, Collider { shape, surface });
384 assert_eq!(w.verify_components(), [("core.collider", e, message.to_owned())]);
385 }
386 }
387
388 #[test]
389 fn a_heightfield_has_a_height_per_sample_and_a_hole_per_cell() {
390 for (f, message) in [
391 (field(1, 2), "1 x 2 samples: a heightfield has at least 2 x 2"),
392 (
393 Heightfield {
394 heights: vec![0.0; 5],
395 ..field(3, 2)
396 },
397 "5 heights for 6 samples",
398 ),
399 (
400 Heightfield {
401 holes: Some(vec![false; 3]),
402 ..field(3, 2)
403 },
404 "3 holes for 2 cells",
405 ),
406 (
407 Heightfield {
408 surfaces: Some(vec![0, 4]),
409 ..field(3, 2)
410 },
411 "surface 4 past the world's 1 surfaces",
412 ),
413 ] {
414 let mut w = world();
415 let e = w.spawn();
416 w.insert(e, f);
417 assert_eq!(
418 w.verify_components(),
419 [("core.heightfield", e, message.to_owned())]
420 );
421 }
422 }
423
424 #[test]
425 fn json_shows_shapes_and_counts() {
426 let c = Collider {
427 shape: Shape::Capsule {
428 a: [0.0; 3],
429 b: [0.0, 1.0, 0.0],
430 radius: 0.25,
431 },
432 surface: 0,
433 };
434 assert_eq!(
435 c.to_json().unwrap(),
436 Some(
437 json!({"shape": {"capsule": {"a": [0.0, 0.0, 0.0], "b": [0.0, 1.0, 0.0], "radius": 0.25}}, "surface": 0})
438 )
439 );
440 let c = Collider {
441 shape: Shape::Convex {
442 points: vec![[0.0; 3]; 4],
443 },
444 surface: 0,
445 };
446 assert_eq!(
447 c.to_json().unwrap(),
448 Some(json!({"shape": {"convex": {"points": 4}}, "surface": 0}))
449 );
450 let h = Heightfield {
451 holes: Some(vec![true, false]),
452 ..field(3, 2)
453 };
454 assert_eq!(
455 h.to_json().unwrap(),
456 Some(
457 json!({"columns": 3, "rows": 2, "spacing": [1.0, 2.0], "heights": 6, "diagonal": "main", "holes": 1, "surfaces": null})
458 )
459 );
460 }
461}