use serde::Serialize;
use serde_json::Value;
use crate::{Component, Entity, JsonError, Transform, World};
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Marker {
pub kind: String,
pub ground: Option<[f32; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub yaw: Option<f32>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Curve {
pub kind: String,
pub points: Vec<[f32; 3]>,
pub closed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub tangents: Option<Vec<[[f32; 3]; 2]>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub up: Option<Vec<[f32; 3]>>,
}
pub fn kind_is_valid(kind: &str) -> bool {
!kind.is_empty()
&& kind
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
fn check_kind(kind: &str) -> Result<(), String> {
if kind_is_valid(kind) {
Ok(())
} else {
Err(format!("kind {kind:?} is not a word of a-z, 0-9 and _"))
}
}
impl Component for Marker {
const NAME: &'static str = "core.marker";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
const VERSION: u32 = 2;
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
check_kind(&self.kind)?;
match (self.ground, w.has::<Transform>(e)) {
(Some(_), true) => Err("both a ground position and a transform".into()),
(None, false) => Err("neither a ground position nor a transform".into()),
(Some(g), false) if !g.iter().all(|v| v.is_finite()) => {
Err(format!("ground position {g:?} is not finite"))
}
_ if self.yaw.is_some() && self.ground.is_none() => {
Err("a yaw without a ground position (a transform gives the facing)".into())
}
_ if self.yaw.is_some_and(|y| !y.is_finite()) => Err("a yaw that is not finite".into()),
_ => Ok(()),
}
}
}
impl Component for Curve {
const NAME: &'static str = "core.curve";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
const VERSION: u32 = 2;
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
check_kind(&self.kind)?;
let n = self.points.len();
if n < 2 {
return Err(format!("{n} points; a curve has at least 2"));
}
if self.closed && self.tangents.is_none() && n < 3 {
return Err(format!("{n} points; a closed polyline has at least 3"));
}
if let Some(i) = self.points.iter().position(|p| !p.iter().all(|v| v.is_finite())) {
return Err(format!("point {i} {:?} is not finite", self.points[i]));
}
if let Some(t) = &self.tangents {
if t.len() != n {
return Err(format!("{} tangent pairs for {n} points", t.len()));
}
if let Some(i) = t.iter().position(|h| !h.iter().flatten().all(|v| v.is_finite())) {
return Err(format!("point {i}'s handles {:?} are not finite", t[i]));
}
}
if let Some(up) = &self.up {
if up.len() != n {
return Err(format!("{} up directions for {n} points", up.len()));
}
let unit = |u: &[f32; 3]| {
let l = u.iter().map(|v| v * v).sum::<f32>().sqrt();
l.is_finite() && (l - 1.0).abs() <= 1e-3
};
if let Some(i) = up.iter().position(|u| !unit(u)) {
return Err(format!("point {i}'s up {:?} is not a unit direction", up[i]));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kinds_are_lower_case_words() {
for good in ["spawn", "trigger_effect", "rail2"] {
assert!(kind_is_valid(good), "{good}");
}
for bad in ["", "Spawn", "fast travel", "a:b", "é"] {
assert!(!kind_is_valid(bad), "{bad}");
}
}
#[test]
fn a_marker_is_placed_by_a_transform_or_a_ground_position() {
let mut w = World::new();
let placed = w.spawn();
w.insert(placed, Transform::IDENTITY);
w.insert(
placed,
Marker {
kind: "spawn".into(),
ground: None,
yaw: None,
},
);
let snapped = w.spawn();
w.insert(
snapped,
Marker {
kind: "travel".into(),
ground: Some([12., 40.]),
yaw: None,
},
);
assert!(w.verify_components().is_empty());
let both = w.spawn();
w.insert(both, Transform::IDENTITY);
w.insert(
both,
Marker {
kind: "travel".into(),
ground: Some([0., 0.]),
yaw: None,
},
);
let neither = w.spawn();
w.insert(
neither,
Marker {
kind: "travel".into(),
ground: None,
yaw: None,
},
);
let nan = w.spawn();
w.insert(
nan,
Marker {
kind: "travel".into(),
ground: Some([f32::NAN, 0.]),
yaw: None,
},
);
let bad_kind = w.spawn();
w.insert(
bad_kind,
Marker {
kind: "Travel".into(),
ground: Some([0., 0.]),
yaw: None,
},
);
let found: Vec<(Entity, String)> = w
.verify_components()
.into_iter()
.map(|(_, e, m)| (e, m))
.collect();
assert_eq!(found.len(), 4, "{found:?}");
assert_eq!(found[0], (both, "both a ground position and a transform".into()));
assert_eq!(
found[1],
(neither, "neither a ground position nor a transform".into())
);
assert!(found[2].1.contains("not finite"), "{found:?}");
assert!(found[3].1.contains("is not a word"), "{found:?}");
}
#[test]
fn a_curve_has_two_finite_points() {
let mut w = World::new();
let curve = |points: Vec<[f32; 3]>| Curve {
kind: "rail".into(),
points,
closed: false,
tangents: None,
up: None,
};
let good = w.spawn();
w.insert(good, curve(vec![[0.; 3], [1., 0., 0.]]));
let short = w.spawn();
w.insert(short, curve(vec![[0.; 3]]));
let nan = w.spawn();
w.insert(nan, curve(vec![[0.; 3], [f32::INFINITY, 0., 0.]]));
let closed_two = w.spawn();
w.insert(
closed_two,
Curve {
closed: true,
..curve(vec![[0.; 3], [1., 0., 0.]])
},
);
let closed_three = w.spawn();
w.insert(
closed_three,
Curve {
closed: true,
..curve(vec![[0.; 3], [1., 0., 0.], [0., 0., 1.]])
},
);
let bezier_two = w.spawn();
w.insert(
bezier_two,
Curve {
closed: true,
tangents: Some(vec![[[0., 0., -1.], [0., 0., 1.]], [[1., 0., 1.], [1., 0., -1.]]]),
..curve(vec![[0.; 3], [1., 0., 0.]])
},
);
let found: Vec<Entity> = w.verify_components().into_iter().map(|(_, e, _)| e).collect();
assert_eq!(found, [short, nan, closed_two]);
assert_eq!(
w.get::<Curve>(good).unwrap().to_json().unwrap(),
Some(
serde_json::json!({"kind": "rail", "points": [[0., 0., 0.], [1., 0., 0.]], "closed": false})
)
);
}
}