Skip to main content

primase_core/
spawn.rs

1//! `place-spawn`: a spawn marker the caller asks for, placed on the read world before a writer plans.
2
3use crate::pass::{Choice, Given, OptionSpec, Options, Pass, PassLog, PassSpec};
4use crate::{Instance, Marker, Name, Transform, World};
5
6/// Where [`PlaceSpawn`] puts the spawn.
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub enum SpawnAt {
9    /// The per-axis median of the placements' ground positions: a few placements far from the rest do not move it
10    /// off the map.
11    Centre,
12    /// This ground position `(east, north)`, metres ([`crate::frame::ground`]).
13    Ground([f32; 2]),
14}
15
16/// `place-spawn{at=centre | east=E,north=N}[,yaw=DEGREES]}`: a `core.marker` of kind `spawn` at a ground position
17/// (its height left to the consumer, which snaps it), facing `yaw` ([`Marker::yaw`]). The caller states where; a
18/// world whose source already places a spawn is refused (the source's own stands). It runs as a selection pass, on
19/// the world a writer plans from, so one spawn serves every part of a conversion; the marker is content of its own
20/// ([`PassLog::place`]). Made by [`PlaceSpawn::centre`] or [`PlaceSpawn::at_ground`], facing [`PlaceSpawn::with_yaw`];
21/// from options by the same calls.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct PlaceSpawn {
24    at: SpawnAt,
25    yaw: Option<f32>,
26}
27
28impl PlaceSpawn {
29    /// A spawn at the median of the placements' ground positions.
30    pub fn centre() -> PlaceSpawn {
31        PlaceSpawn {
32            at: SpawnAt::Centre,
33            yaw: None,
34        }
35    }
36
37    /// A spawn at this ground position; finite metres.
38    pub fn at_ground(east: f32, north: f32) -> Result<PlaceSpawn, String> {
39        if !(east.is_finite() && north.is_finite()) {
40            return Err(format!("({east}, {north}) is not a finite ground position"));
41        }
42        Ok(PlaceSpawn {
43            at: SpawnAt::Ground([east, north]),
44            yaw: None,
45        })
46    }
47
48    /// Facing `degrees` (finite) about the up axis ([`Marker::yaw`]).
49    pub fn with_yaw(mut self, degrees: f32) -> Result<PlaceSpawn, String> {
50        if !degrees.is_finite() {
51            return Err(format!("yaw {degrees} is not finite"));
52        }
53        self.yaw = Some(degrees);
54        Ok(self)
55    }
56
57    pub fn at(&self) -> SpawnAt {
58        self.at
59    }
60
61    pub fn yaw(&self) -> Option<f32> {
62        self.yaw
63    }
64
65    /// From its options (`at=centre`, or `east` and `north`; `yaw`), through the same constructors.
66    fn from_given(g: &Given) -> Result<PlaceSpawn, String> {
67        let number = |k: &str| {
68            g.optional(k)
69                .map(|v| v.parse::<f32>().map_err(|_| format!("{k}={v} is not a number")))
70                .transpose()
71        };
72        let (east, north, yaw) = (number("east")?, number("north")?, number("yaw")?);
73        let pass = match (g.optional_choice::<At>("at"), east, north) {
74            (Some(At::Centre), None, None) => PlaceSpawn::centre(),
75            (Some(At::Centre), _, Some(_)) => return Err("at=centre or east, north: not both".into()),
76            (None, Some(e), Some(n)) => PlaceSpawn::at_ground(e, n)?,
77            _ => return Err("east and north go together".into()),
78        };
79        match yaw {
80            Some(y) => pass.with_yaw(y),
81            None => Ok(pass),
82        }
83    }
84}
85
86/// `place-spawn`'s `at`: the one place it names.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88enum At {
89    Centre,
90}
91
92impl Choice for At {
93    const VALUES: &'static [(&'static str, Self)] = &[("centre", At::Centre)];
94}
95
96/// `place-spawn`'s declaration ([`crate::pass::PassRegistry::add`]): where the spawn goes is the program's, no
97/// default.
98pub const PLACE_SPAWN: PassSpec = PassSpec {
99    name: "place-spawn",
100    options: &[
101        OptionSpec::one_of_choice::<At>(
102            "place",
103            "at",
104            "centre: the median of the placements' ground positions",
105        ),
106        OptionSpec::one_of(
107            "place",
108            "east",
109            "0",
110            "metres, with north: the spawn's ground position",
111        ),
112        OptionSpec::optional(
113            "north",
114            "0",
115            "metres, with east: the spawn's ground position",
116            "required with east, not taken with at",
117        ),
118        OptionSpec::optional("yaw", "90", "degrees: the spawn's heading", "no heading"),
119    ],
120    make: |g| Ok(Box::new(PlaceSpawn::from_given(g)?)),
121};
122
123impl Pass for PlaceSpawn {
124    fn name(&self) -> &'static str {
125        "place-spawn"
126    }
127    fn options(&self) -> Options {
128        let mut o = Options::new();
129        match self.at {
130            SpawnAt::Centre => {
131                o.insert("at".into(), "centre".into());
132            }
133            SpawnAt::Ground([e, n]) => {
134                o.insert("east".into(), e.to_string());
135                o.insert("north".into(), n.to_string());
136            }
137        }
138        if let Some(y) = self.yaw {
139            o.insert("yaw".into(), y.to_string());
140        }
141        o
142    }
143    fn provides(&self) -> &[&'static str] {
144        &["core.marker", "core.name"]
145    }
146    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
147        if let Some((e, _)) = w.iter::<Marker>().find(|(_, m)| m.kind == "spawn") {
148            return Err(format!(
149                "the world already has a spawn ({e:?}): the source's own stands; leave out place-spawn"
150            ));
151        }
152        let ground = match self.at {
153            SpawnAt::Ground(g) => g,
154            SpawnAt::Centre => centre_ground(w, "place-spawn")?,
155        };
156        let e = w.spawn();
157        w.insert(e, Name("spawn".into()));
158        w.insert(
159            e,
160            Marker {
161                kind: "spawn".into(),
162                ground: Some(ground),
163                yaw: self.yaw,
164            },
165        );
166        log.place(e);
167        log.count("spawn markers placed", 1);
168        Ok(())
169    }
170}
171
172/// The per-axis median of the placements' ground positions (east, north): a few placements far from the rest do not
173/// move it off the map. A placement without a transform has no place, so it is refused (naming `pass`) rather than
174/// counted as the origin.
175pub fn centre_ground(w: &World, pass: &str) -> Result<[f32; 2], String> {
176    let mut east = Vec::new();
177    let mut north = Vec::new();
178    for (e, _) in w.iter::<Instance>() {
179        let t = w.get::<Transform>(e).ok_or_else(|| {
180            format!("{pass} at=centre: placement {e:?} has no core.transform, so no place to count")
181        })?;
182        let [x, n] = crate::frame::ground(t.cols[3]);
183        east.push(x);
184        north.push(n);
185    }
186    if east.is_empty() {
187        return Err(format!(
188            "{pass} at=centre: the world has no placement to centre on"
189        ));
190    }
191    east.sort_by(f32::total_cmp);
192    north.sort_by(f32::total_cmp);
193    Ok([median(&east), median(&north)])
194}
195
196/// The median of sorted, nonempty `v` (the mean of the middle two for an even count).
197fn median(v: &[f32]) -> f32 {
198    let n = v.len();
199    if n % 2 == 1 {
200        v[n / 2]
201    } else {
202        (v[n / 2 - 1] + v[n / 2]) / 2.0
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::pass::{PassManager, PassRegistry, Pipeline, RunReport};
210
211    fn placed(w: &mut World, east: f32, north: f32) {
212        let proto = w.spawn();
213        let e = w.spawn();
214        w.insert(
215            e,
216            Instance {
217                prototype: proto,
218                variant: 0,
219            },
220        );
221        let mut t = Transform::IDENTITY;
222        // ground (east, north) is (x, -z)
223        t.cols[3] = [east, 0.0, -north];
224        w.insert(e, t);
225    }
226
227    fn run(w: &mut World, text: &str) -> Result<RunReport, String> {
228        let mut reg = PassRegistry::new();
229        reg.add(&PLACE_SPAWN);
230        let pipeline: Pipeline = reg.parse(text).map_err(|e| e.to_string())?;
231        let mut report = RunReport::default();
232        PassManager::new(reg)
233            .run(w, &pipeline, &mut report)
234            .map_err(|e| e.to_string())?;
235        Ok(report)
236    }
237
238    /// Worked by hand: placements at east 0, 10, 20 and 1,000 (an outlier) and north 0, 0, 40, 40 have medians east 15
239    /// (the mean of 10 and 20) and north 20; the spawn is a ground marker there, facing yaw 90, placed by the pass.
240    #[test]
241    fn the_centre_is_the_median_of_the_placements() {
242        let mut w = World::new();
243        for (e, n) in [(0., 0.), (10., 0.), (20., 40.), (1000., 40.)] {
244            placed(&mut w, e, n);
245        }
246        let report = run(&mut w, "place-spawn{at=centre,yaw=90}").unwrap();
247        let (e, m) = w.iter::<Marker>().next().unwrap();
248        assert_eq!(
249            m,
250            &Marker {
251                kind: "spawn".into(),
252                ground: Some([15.0, 20.0]),
253                yaw: Some(90.0),
254            }
255        );
256        assert!(!w.has::<Transform>(e));
257        assert!(report.steps[0].log.placed.contains(&e));
258        assert!(w.verify_components().is_empty());
259    }
260
261    /// A placement without a transform has no place: the centre refuses it, naming it, rather than counting the origin.
262    #[test]
263    fn a_placement_without_a_transform_is_refused() {
264        let mut w = World::new();
265        placed(&mut w, 10., 10.);
266        let proto = w.spawn();
267        let bare = w.spawn();
268        w.insert(
269            bare,
270            Instance {
271                prototype: proto,
272                variant: 0,
273            },
274        );
275        let err = run(&mut w, "place-spawn{at=centre}").unwrap_err();
276        assert!(
277            err.contains("has no core.transform") && err.contains(&format!("{bare:?}")),
278            "{err}"
279        );
280        assert!(w.iter::<Marker>().next().is_none());
281    }
282
283    #[test]
284    fn a_given_position_a_source_spawn_and_bad_options() {
285        let mut w = World::new();
286        run(&mut w, "place-spawn{east=3,north=-4}").unwrap();
287        let (_, m) = w.iter::<Marker>().next().unwrap();
288        assert_eq!((m.ground, m.yaw), (Some([3.0, -4.0]), None));
289        // the source's own spawn stands
290        assert!(
291            run(&mut w, "place-spawn{at=centre}")
292                .unwrap_err()
293                .contains("already has a spawn")
294        );
295        // nothing to centre on
296        // a position or yaw that is not a finite number is refused, typed or from options
297        assert!(PlaceSpawn::at_ground(f32::NAN, 0.0).is_err());
298        assert!(PlaceSpawn::centre().with_yaw(f32::INFINITY).is_err());
299        assert!(run(&mut World::new(), "place-spawn{east=inf,north=0}").is_err());
300        assert!(
301            run(&mut World::new(), "place-spawn{at=centre}")
302                .unwrap_err()
303                .contains("no placement")
304        );
305        // where it goes is the program's: no option is refused, naming the choice
306        assert!(
307            run(&mut World::new(), "place-spawn")
308                .unwrap_err()
309                .contains("one of at or east is required"),
310        );
311        for bad in [
312            "place-spawn{east=3}",
313            "place-spawn{north=3}",
314            "place-spawn{yaw=90}",
315            "place-spawn{at=centre,east=1,north=2}",
316            "place-spawn{at=middle}",
317            "place-spawn{yaw=nan,at=centre}",
318            "place-spawn{size=2}",
319        ] {
320            assert!(run(&mut World::new(), bad).is_err(), "{bad}");
321        }
322    }
323}