Skip to main content

primase_core/
marker.rs

1//! What a source places for players and tools rather than for drawing: points of interest (`core.marker`) and
2//! polylines (`core.curve`). Any reader that reads such data gives them; what only its dialect knows (how a marker
3//! snaps to the ground, a rail's grind surface) stays in its dialect's component on the same entity.
4
5use serde::Serialize;
6use serde_json::Value;
7
8use crate::{Component, Entity, JsonError, Transform, World};
9
10/// `core.marker`: an author-placed point of interest: a spawn, a fast-travel stop, a trainer, an effect emitter, a
11/// waypoint. Where: the entity's [`Transform`] (position, and orientation when the source gives one: the marker faces
12/// its local +Z). A source that leaves the height to be found (snapped to the ground by its consumer) gives no
13/// transform but [`Marker::ground`]. Name: the entity's `core.name`.
14#[derive(Clone, Debug, PartialEq, Serialize)]
15pub struct Marker {
16    /// A lower-case word from the source's vocabulary (see [`kind_is_valid`]); writers that do not know a kind still
17    /// show the marker as a point.
18    pub kind: String,
19    /// The ground position `(east, north)`, metres ([`crate::frame::ground`]), when the source states no height: the
20    /// entity then has no transform.
21    pub ground: Option<[f32; 2]>,
22    /// A ground marker's facing, which no transform gives it: degrees about +Y, 0 facing +Z and 90 facing +X, as a
23    /// marker with a transform faces its local +Z. `None`: the source states none.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub yaw: Option<f32>,
26}
27
28/// `core.curve`: an author-placed curve in world space: a grind rail, a ledge, a coping, a path, a chairlift's cable.
29/// Points in order, metres; `closed`: the last point joins the first. Without `tangents` it is the polyline through
30/// them; with them each pair of consecutive points is a cubic Bezier segment from a point by its out handle and the
31/// next point's in handle (Catmull-Rom, Hermite and auto tangents convert to these handles exactly). A consumer that
32/// needs a polyline runs `flatten-curves`.
33#[derive(Clone, Debug, PartialEq, Serialize)]
34pub struct Curve {
35    /// As [`Marker::kind`].
36    pub kind: String,
37    pub points: Vec<[f32; 3]>,
38    pub closed: bool,
39    /// Each point's in and out handles, absolute positions.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub tangents: Option<Vec<[[f32; 3]; 2]>>,
42    /// Each point's up direction (a rail's or a cable car's roll), unit length.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub up: Option<Vec<[f32; 3]>>,
45}
46
47/// A kind is a nonempty word of `a-z`, `0-9` and `_`. The known ones are listed in the format documentation; any
48/// other is a source's own.
49pub fn kind_is_valid(kind: &str) -> bool {
50    !kind.is_empty()
51        && kind
52            .bytes()
53            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
54}
55
56fn check_kind(kind: &str) -> Result<(), String> {
57    if kind_is_valid(kind) {
58        Ok(())
59    } else {
60        Err(format!("kind {kind:?} is not a word of a-z, 0-9 and _"))
61    }
62}
63
64impl Component for Marker {
65    const NAME: &'static str = "core.marker";
66    const GEOMETRY: crate::Geometry = crate::Geometry::None;
67    /// 2: a ground marker's `yaw`.
68    const VERSION: u32 = 2;
69    /// Placed by its entity's transform, which the pass keeps rigid; a ground position is world space.
70    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
71        crate::world::Linearity::Invariant
72    }
73    fn to_json(&self) -> Result<Option<Value>, JsonError> {
74        crate::json::to_value(self).map(Some)
75    }
76    /// A valid kind, and exactly one of a transform and a finite ground position.
77    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
78        check_kind(&self.kind)?;
79        match (self.ground, w.has::<Transform>(e)) {
80            (Some(_), true) => Err("both a ground position and a transform".into()),
81            (None, false) => Err("neither a ground position nor a transform".into()),
82            (Some(g), false) if !g.iter().all(|v| v.is_finite()) => {
83                Err(format!("ground position {g:?} is not finite"))
84            }
85            _ if self.yaw.is_some() && self.ground.is_none() => {
86                Err("a yaw without a ground position (a transform gives the facing)".into())
87            }
88            _ if self.yaw.is_some_and(|y| !y.is_finite()) => Err("a yaw that is not finite".into()),
89            _ => Ok(()),
90        }
91    }
92}
93
94impl Component for Curve {
95    const NAME: &'static str = "core.curve";
96    const GEOMETRY: crate::Geometry = crate::Geometry::None;
97    /// 2: optional Bezier `tangents` and `up` per point.
98    const VERSION: u32 = 2;
99    /// World space: not in any entity's local space.
100    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
101        crate::world::Linearity::Invariant
102    }
103    fn to_json(&self) -> Result<Option<Value>, JsonError> {
104        crate::json::to_value(self).map(Some)
105    }
106    /// A valid kind and at least two finite points (three for a closed polyline, whose two would only retrace one
107    /// segment; two Bezier segments do enclose a shape); handles and up directions, when given, one per point and
108    /// finite (up directions of unit length within 1e-3).
109    fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
110        check_kind(&self.kind)?;
111        let n = self.points.len();
112        if n < 2 {
113            return Err(format!("{n} points; a curve has at least 2"));
114        }
115        if self.closed && self.tangents.is_none() && n < 3 {
116            return Err(format!("{n} points; a closed polyline has at least 3"));
117        }
118        if let Some(i) = self.points.iter().position(|p| !p.iter().all(|v| v.is_finite())) {
119            return Err(format!("point {i} {:?} is not finite", self.points[i]));
120        }
121        if let Some(t) = &self.tangents {
122            if t.len() != n {
123                return Err(format!("{} tangent pairs for {n} points", t.len()));
124            }
125            if let Some(i) = t.iter().position(|h| !h.iter().flatten().all(|v| v.is_finite())) {
126                return Err(format!("point {i}'s handles {:?} are not finite", t[i]));
127            }
128        }
129        if let Some(up) = &self.up {
130            if up.len() != n {
131                return Err(format!("{} up directions for {n} points", up.len()));
132            }
133            let unit = |u: &[f32; 3]| {
134                let l = u.iter().map(|v| v * v).sum::<f32>().sqrt();
135                l.is_finite() && (l - 1.0).abs() <= 1e-3
136            };
137            if let Some(i) = up.iter().position(|u| !unit(u)) {
138                return Err(format!("point {i}'s up {:?} is not a unit direction", up[i]));
139            }
140        }
141        Ok(())
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn kinds_are_lower_case_words() {
151        for good in ["spawn", "trigger_effect", "rail2"] {
152            assert!(kind_is_valid(good), "{good}");
153        }
154        for bad in ["", "Spawn", "fast travel", "a:b", "é"] {
155            assert!(!kind_is_valid(bad), "{bad}");
156        }
157    }
158
159    #[test]
160    fn a_marker_is_placed_by_a_transform_or_a_ground_position() {
161        let mut w = World::new();
162        let placed = w.spawn();
163        w.insert(placed, Transform::IDENTITY);
164        w.insert(
165            placed,
166            Marker {
167                kind: "spawn".into(),
168                ground: None,
169                yaw: None,
170            },
171        );
172        let snapped = w.spawn();
173        w.insert(
174            snapped,
175            Marker {
176                kind: "travel".into(),
177                ground: Some([12., 40.]),
178                yaw: None,
179            },
180        );
181        assert!(w.verify_components().is_empty());
182        let both = w.spawn();
183        w.insert(both, Transform::IDENTITY);
184        w.insert(
185            both,
186            Marker {
187                kind: "travel".into(),
188                ground: Some([0., 0.]),
189                yaw: None,
190            },
191        );
192        let neither = w.spawn();
193        w.insert(
194            neither,
195            Marker {
196                kind: "travel".into(),
197                ground: None,
198                yaw: None,
199            },
200        );
201        let nan = w.spawn();
202        w.insert(
203            nan,
204            Marker {
205                kind: "travel".into(),
206                ground: Some([f32::NAN, 0.]),
207                yaw: None,
208            },
209        );
210        let bad_kind = w.spawn();
211        w.insert(
212            bad_kind,
213            Marker {
214                kind: "Travel".into(),
215                ground: Some([0., 0.]),
216                yaw: None,
217            },
218        );
219        let found: Vec<(Entity, String)> = w
220            .verify_components()
221            .into_iter()
222            .map(|(_, e, m)| (e, m))
223            .collect();
224        assert_eq!(found.len(), 4, "{found:?}");
225        assert_eq!(found[0], (both, "both a ground position and a transform".into()));
226        assert_eq!(
227            found[1],
228            (neither, "neither a ground position nor a transform".into())
229        );
230        assert!(found[2].1.contains("not finite"), "{found:?}");
231        assert!(found[3].1.contains("is not a word"), "{found:?}");
232    }
233
234    #[test]
235    fn a_curve_has_two_finite_points() {
236        let mut w = World::new();
237        let curve = |points: Vec<[f32; 3]>| Curve {
238            kind: "rail".into(),
239            points,
240            closed: false,
241            tangents: None,
242            up: None,
243        };
244        let good = w.spawn();
245        w.insert(good, curve(vec![[0.; 3], [1., 0., 0.]]));
246        let short = w.spawn();
247        w.insert(short, curve(vec![[0.; 3]]));
248        let nan = w.spawn();
249        w.insert(nan, curve(vec![[0.; 3], [f32::INFINITY, 0., 0.]]));
250        // closed: a polyline needs a third point; two Bezier points enclose a shape
251        let closed_two = w.spawn();
252        w.insert(
253            closed_two,
254            Curve {
255                closed: true,
256                ..curve(vec![[0.; 3], [1., 0., 0.]])
257            },
258        );
259        let closed_three = w.spawn();
260        w.insert(
261            closed_three,
262            Curve {
263                closed: true,
264                ..curve(vec![[0.; 3], [1., 0., 0.], [0., 0., 1.]])
265            },
266        );
267        let bezier_two = w.spawn();
268        w.insert(
269            bezier_two,
270            Curve {
271                closed: true,
272                tangents: Some(vec![[[0., 0., -1.], [0., 0., 1.]], [[1., 0., 1.], [1., 0., -1.]]]),
273                ..curve(vec![[0.; 3], [1., 0., 0.]])
274            },
275        );
276        let found: Vec<Entity> = w.verify_components().into_iter().map(|(_, e, _)| e).collect();
277        assert_eq!(found, [short, nan, closed_two]);
278        assert_eq!(
279            w.get::<Curve>(good).unwrap().to_json().unwrap(),
280            Some(
281                serde_json::json!({"kind": "rail", "points": [[0., 0., 0.], [1., 0., 0.]], "closed": false})
282            )
283        );
284    }
285}