1use crate::pass::{Choice, Given, OptionSpec, Options, Pass, PassLog, PassSpec};
4use crate::{Instance, Marker, Name, Transform, World};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
8pub enum SpawnAt {
9 Centre,
12 Ground([f32; 2]),
14}
15
16#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct PlaceSpawn {
24 at: SpawnAt,
25 yaw: Option<f32>,
26}
27
28impl PlaceSpawn {
29 pub fn centre() -> PlaceSpawn {
31 PlaceSpawn {
32 at: SpawnAt::Centre,
33 yaw: None,
34 }
35 }
36
37 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 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 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#[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
96pub 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
172pub 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
196fn 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 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 #[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 #[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 assert!(
291 run(&mut w, "place-spawn{at=centre}")
292 .unwrap_err()
293 .contains("already has a spawn")
294 );
295 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 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}