use serde::Serialize;
use serde_json::{Value, json};
use crate::{Component, Entity, JsonError, Surfaces, World};
#[derive(Clone, Debug, PartialEq)]
pub struct Collider {
pub shape: Shape,
pub surface: u16,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Shape {
Box {
center: [f32; 3],
half_extents: [f32; 3],
},
Sphere {
center: [f32; 3],
radius: f32,
},
Capsule {
a: [f32; 3],
b: [f32; 3],
radius: f32,
},
Cylinder {
a: [f32; 3],
b: [f32; 3],
radius: f32,
},
Convex {
points: Vec<[f32; 3]>,
},
Heightfield,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Diagonal {
Main,
Anti,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Heightfield {
pub columns: u32,
pub rows: u32,
pub spacing: [f32; 2],
pub heights: Vec<f32>,
pub diagonal: Diagonal,
pub holes: Option<Vec<bool>>,
pub surfaces: Option<Vec<u16>>,
}
impl Heightfield {
pub fn cells(&self) -> usize {
(self.columns.saturating_sub(1) as usize) * (self.rows.saturating_sub(1) as usize)
}
}
fn finite(values: impl IntoIterator<Item = f32>, what: &str) -> Result<(), String> {
match values.into_iter().find(|v| !v.is_finite()) {
Some(v) => Err(format!("{what}: {v} is not finite")),
None => Ok(()),
}
}
fn positive(v: f32, what: &str) -> Result<(), String> {
if v.is_finite() && v > 0.0 {
Ok(())
} else {
Err(format!("{what} {v} is not positive"))
}
}
fn surface_known(w: &World, s: u16) -> Result<(), String> {
let known = w.resource::<Surfaces>().map_or(0, |t| t.0.len());
if usize::from(s) < known {
Ok(())
} else {
Err(format!("surface {s} past the world's {known} surfaces"))
}
}
impl Component for Collider {
const NAME: &'static str = "core.collider";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
use crate::world::Linearity;
let scaled = |r: f32, s: f64| (f64::from(r) * s) as f32;
let shape = match &self.shape {
Shape::Convex { points } => Shape::Convex {
points: points.iter().map(|&p| l.point(p)).collect(),
},
Shape::Box { center, half_extents } => {
let corners: Vec<[f32; 3]> = (0..8)
.map(|k| {
let sign = |bit: usize| if k & (1 << bit) == 0 { -1.0 } else { 1.0 };
l.point(std::array::from_fn(|i| center[i] + sign(i) * half_extents[i]))
})
.collect();
if l.axis_map().is_some() {
let lo: [f32; 3] =
std::array::from_fn(|i| corners.iter().map(|c| c[i]).fold(f32::INFINITY, f32::min));
let hi: [f32; 3] = std::array::from_fn(|i| {
corners.iter().map(|c| c[i]).fold(f32::NEG_INFINITY, f32::max)
});
Shape::Box {
center: l.point(*center),
half_extents: std::array::from_fn(|i| (hi[i] - lo[i]) / 2.0),
}
} else {
Shape::Convex { points: corners }
}
}
Shape::Sphere { center, radius } => match l.similarity() {
Some(s) => Shape::Sphere {
center: l.point(*center),
radius: scaled(*radius, s),
},
None => return Linearity::Unsupported,
},
Shape::Capsule { a, b, radius } => match l.similarity() {
Some(s) => Shape::Capsule {
a: l.point(*a),
b: l.point(*b),
radius: scaled(*radius, s),
},
None => return Linearity::Unsupported,
},
Shape::Cylinder { a, b, radius } => match l.similarity() {
Some(s) => Shape::Cylinder {
a: l.point(*a),
b: l.point(*b),
radius: scaled(*radius, s),
},
None => return Linearity::Unsupported,
},
Shape::Heightfield => return Linearity::Invariant,
};
Linearity::Applied(Collider {
shape,
surface: self.surface,
})
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
let shape = match &self.shape {
Shape::Convex { points } => json!({"convex": {"points": points.len()}}),
other => crate::json::to_value(other)?,
};
Ok(Some(json!({"shape": shape, "surface": self.surface})))
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
surface_known(w, self.surface)?;
match &self.shape {
Shape::Box { center, half_extents } => {
finite(*center, "box centre")?;
half_extents
.iter()
.try_for_each(|&h| positive(h, "box half extent"))
}
Shape::Sphere { center, radius } => {
finite(*center, "sphere centre")?;
positive(*radius, "sphere radius")
}
Shape::Capsule { a, b, radius } | Shape::Cylinder { a, b, radius } => {
finite(a.iter().chain(b).copied(), "axis ends")?;
positive(*radius, "radius")
}
Shape::Convex { points } => {
if points.is_empty() {
return Err("a convex shape without points".into());
}
finite(points.iter().flatten().copied(), "convex points")
}
Shape::Heightfield => {
if w.has::<Heightfield>(e) {
Ok(())
} else {
Err("a heightfield shape without the entity's core.heightfield".into())
}
}
}
}
}
impl Component for Heightfield {
const NAME: &'static str = "core.heightfield";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn apply_linear(&self, l: &crate::Linear) -> crate::world::Linearity<Self> {
use crate::world::Linearity;
let col = |k: usize| l.point(std::array::from_fn(|i| if i == k { 1.0 } else { 0.0 }));
let (x, y, z) = (col(0), col(1), col(2));
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;
if !(diagonal && x[0] > 0.0 && y[1] > 0.0 && z[2] > 0.0) {
return Linearity::Unsupported;
}
let scale = |v: f32, s: f32| (f64::from(v) * f64::from(s)) as f32;
let mut h = self.clone();
h.spacing = [scale(h.spacing[0], x[0]), scale(h.spacing[1], z[2])];
h.heights.iter_mut().for_each(|v| *v = scale(*v, y[1]));
Linearity::Applied(h)
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
Ok(Some(json!({
"columns": self.columns,
"rows": self.rows,
"spacing": crate::json::to_value(&self.spacing)?,
"heights": self.heights.len(),
"diagonal": crate::json::to_value(&self.diagonal)?,
"holes": self.holes.as_ref().map(|h| h.iter().filter(|&&x| x).count()),
"surfaces": self.surfaces.as_ref().map(Vec::len),
})))
}
fn verify(&self, _e: Entity, w: &World) -> Result<(), String> {
if self.columns < 2 || self.rows < 2 {
return Err(format!(
"{} x {} samples: a heightfield has at least 2 x 2",
self.columns, self.rows
));
}
self.spacing.iter().try_for_each(|&s| positive(s, "spacing"))?;
let samples = self.columns as usize * self.rows as usize;
if self.heights.len() != samples {
return Err(format!("{} heights for {samples} samples", self.heights.len()));
}
finite(self.heights.iter().copied(), "heights")?;
let cells = self.cells();
if let Some(h) = &self.holes
&& h.len() != cells
{
return Err(format!("{} holes for {cells} cells", h.len()));
}
if let Some(s) = &self.surfaces {
if s.len() != cells {
return Err(format!("{} surfaces for {cells} cells", s.len()));
}
s.iter().try_for_each(|&s| surface_known(w, s))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn world() -> World {
let mut w = World::new();
let mut s = Surfaces::default();
s.index("concrete").unwrap();
w.insert_resource(s);
w
}
fn field(columns: u32, rows: u32) -> Heightfield {
Heightfield {
columns,
rows,
spacing: [1.0, 2.0],
heights: vec![0.0; (columns * rows) as usize],
diagonal: Diagonal::Main,
holes: None,
surfaces: None,
}
}
#[test]
fn shapes_verify_their_sizes_surfaces_and_heightfield() {
let mut w = world();
let ok = [
Shape::Box {
center: [0.0; 3],
half_extents: [1.0, 2.0, 3.0],
},
Shape::Sphere {
center: [0.0; 3],
radius: 0.5,
},
Shape::Capsule {
a: [0.0; 3],
b: [0.0, 2.0, 0.0],
radius: 0.5,
},
Shape::Convex {
points: vec![[0.0; 3]],
},
];
for shape in ok {
let e = w.spawn();
w.insert(e, Collider { shape, surface: 0 });
}
let terrain = w.spawn();
w.insert(terrain, field(3, 2));
w.insert(
terrain,
Collider {
shape: Shape::Heightfield,
surface: 0,
},
);
assert!(w.verify_components().is_empty(), "{:?}", w.verify_components());
let bad = [
(
Shape::Sphere {
center: [0.0; 3],
radius: 0.0,
},
0,
"sphere radius 0 is not positive",
),
(
Shape::Box {
center: [f32::NAN, 0.0, 0.0],
half_extents: [1.0; 3],
},
0,
"box centre: NaN is not finite",
),
(
Shape::Convex { points: Vec::new() },
0,
"a convex shape without points",
),
(
Shape::Heightfield,
0,
"a heightfield shape without the entity's core.heightfield",
),
(
Shape::Sphere {
center: [0.0; 3],
radius: 1.0,
},
1,
"surface 1 past the world's 1 surfaces",
),
];
for (shape, surface, message) in bad {
let mut w = world();
let e = w.spawn();
w.insert(e, Collider { shape, surface });
assert_eq!(w.verify_components(), [("core.collider", e, message.to_owned())]);
}
}
#[test]
fn a_heightfield_has_a_height_per_sample_and_a_hole_per_cell() {
for (f, message) in [
(field(1, 2), "1 x 2 samples: a heightfield has at least 2 x 2"),
(
Heightfield {
heights: vec![0.0; 5],
..field(3, 2)
},
"5 heights for 6 samples",
),
(
Heightfield {
holes: Some(vec![false; 3]),
..field(3, 2)
},
"3 holes for 2 cells",
),
(
Heightfield {
surfaces: Some(vec![0, 4]),
..field(3, 2)
},
"surface 4 past the world's 1 surfaces",
),
] {
let mut w = world();
let e = w.spawn();
w.insert(e, f);
assert_eq!(
w.verify_components(),
[("core.heightfield", e, message.to_owned())]
);
}
}
#[test]
fn json_shows_shapes_and_counts() {
let c = Collider {
shape: Shape::Capsule {
a: [0.0; 3],
b: [0.0, 1.0, 0.0],
radius: 0.25,
},
surface: 0,
};
assert_eq!(
c.to_json().unwrap(),
Some(
json!({"shape": {"capsule": {"a": [0.0, 0.0, 0.0], "b": [0.0, 1.0, 0.0], "radius": 0.25}}, "surface": 0})
)
);
let c = Collider {
shape: Shape::Convex {
points: vec![[0.0; 3]; 4],
},
surface: 0,
};
assert_eq!(
c.to_json().unwrap(),
Some(json!({"shape": {"convex": {"points": 4}}, "surface": 0}))
);
let h = Heightfield {
holes: Some(vec![true, false]),
..field(3, 2)
};
assert_eq!(
h.to_json().unwrap(),
Some(
json!({"columns": 3, "rows": 2, "spacing": [1.0, 2.0], "heights": 6, "diagonal": "main", "holes": 1, "surfaces": null})
)
);
}
}