delvewright_dsl/siteplan/place.rs
1//! The plan resolved once — every box's footprint, plane and headroom —
2//! and the resolved plan in world cells that the checks, the derivation and the
3//! battery all read: placed boxes, placed seams, and the stair run.
4
5use super::*;
6
7// ---------------------------------------------------------------------------
8// Resolution — the plan read once, so no check re-derives a number
9// ---------------------------------------------------------------------------
10
11/// One box with everything the checks below need already worked out.
12///
13/// **The model every geometric check depends on** is stated where an author
14/// reads it — on [`PlanBox`], whose schema description carries it — and the
15/// number itself is [`SHARED_FACE_GAP_CELLS`]. In short: a box is the **play
16/// space** of a place, the shell stands in the one-cell gap between two
17/// neighbours, `extent` is therefore the interior footprint the author
18/// declared, and two connected places sit exactly
19/// [`SHARED_FACE_GAP_CELLS`] apart on the face they share (`DW0828`).
20#[derive(Debug, Clone)]
21pub(super) struct Placed<'a> {
22 pub(super) index: usize,
23 pub(super) plan: &'a PlanBox,
24 /// Footprint, inclusive: `[x0, x1, z0, z1]`.
25 pub(super) foot: [i64; 4],
26 /// The walk plane.
27 pub(super) floor: i64,
28 /// Cells of headroom over the walk plane — the lid's clearance, or the
29 /// sky-open place's declared courses of air.
30 pub(super) clearance: u32,
31 /// How its corner was obtained (spec-0059 §3).
32 pub(super) by: Provenance,
33}
34
35impl Placed<'_> {
36 pub(super) fn x0(&self) -> i64 {
37 self.foot[0]
38 }
39 pub(super) fn x1(&self) -> i64 {
40 self.foot[1]
41 }
42 pub(super) fn z0(&self) -> i64 {
43 self.foot[2]
44 }
45 pub(super) fn z1(&self) -> i64 {
46 self.foot[3]
47 }
48
49 /// The inclusive vertical span of the play space.
50 pub(super) fn y_span(&self) -> (i64, i64) {
51 (self.floor, self.floor + i64::from(self.clearance) - 1)
52 }
53
54 /// The centre of the footprint, in blocks.
55 pub(super) fn centre_xz(&self) -> (f64, f64) {
56 (
57 (self.x0() as f64 + self.x1() as f64) / 2.0,
58 (self.z0() as f64 + self.z1() as f64) / 2.0,
59 )
60 }
61}
62
63// ---------------------------------------------------------------------------
64// The resolved plan, in world cells — ONE authority, three readers
65// ---------------------------------------------------------------------------
66
67/// One place, resolved into world cells: the play space the plan gives it.
68///
69/// Public because three readers need the same answer and two of them are in
70/// another crate: the stage-4 checks here, the **blockout derivation** that
71/// builds the mass, and the **stage-5 battery** that judges the built bytes
72/// against the plan. Two of those computing "where is this box" independently is
73/// how a builder and its observer come to agree about a world neither of them
74/// describes.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct PlacedBox {
77 /// The place this embeds.
78 pub node: NodeId,
79 /// Inclusive footprint `[x0, x1, z0, z1]`.
80 pub foot: [i64; 4],
81 /// The walk plane's world `y`.
82 pub floor: i64,
83 /// Cells of headroom over the walk plane.
84 pub clearance: u32,
85 /// True when the plan declared the place sky-open — a courtyard, a shore,
86 /// a summit. It claims exactly its declared courses of air (which is what
87 /// [`PlacedBox::clearance`] holds); what it makes no claim on is the air
88 /// above them.
89 pub open: bool,
90 /// What it stands on: the site's ground, or hung `n` courses under its
91 /// floor course (spec-0098 §2).
92 pub base: super::Base,
93 /// The roof the plan reserves over this place (spec-0098 §3), when it
94 /// declares one. Never present on an open box (`DW0988`).
95 pub roof: Option<super::Roof>,
96}
97
98impl PlacedBox {
99 /// The play space's inclusive world AABB.
100 #[must_use]
101 pub fn space(&self) -> ([i64; 3], [i64; 3]) {
102 (
103 [self.foot[0], self.floor, self.foot[2]],
104 [
105 self.foot[1],
106 self.floor + i64::from(self.clearance) - 1,
107 self.foot[3],
108 ],
109 )
110 }
111
112 /// The floor centre — where a body seated in this place stands.
113 #[must_use]
114 pub fn centre(&self) -> [i64; 3] {
115 [
116 (self.foot[0] + self.foot[1]) / 2,
117 self.floor,
118 (self.foot[2] + self.foot[3]) / 2,
119 ]
120 }
121}
122
123/// One connection, resolved into world cells: the wall the two places share and
124/// the hole the plan cut in it.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct PlacedSeam {
127 /// The connection this allocates.
128 pub edge: EdgeId,
129 /// Its class, as the graph spells it.
130 pub class: &'static str,
131 /// The `a` end.
132 pub a: NodeId,
133 /// The `b` end.
134 pub b: NodeId,
135 /// Which face **of `a`** the seam sits on.
136 pub face: Face,
137 /// The axis the shared wall is flat in: 0 = x, 1 = y, 2 = z.
138 pub normal_axis: usize,
139 /// The wall's coordinate on that axis — one cell thick, so one number.
140 pub plane: i64,
141 /// The opening's inclusive world AABB (flat in [`Self::normal_axis`]).
142 pub opening: ([i64; 3], [i64; 3]),
143 /// The whole rectangle the two boxes share on that wall, inclusive.
144 pub shared: ([i64; 3], [i64; 3]),
145 /// Which kind of connection this seam allocates (spec-0053 §4). A portal's
146 /// `opening` is a standard's rectangle; a contact's is its span.
147 pub crossing: Crossing,
148 /// `floor(b) − floor(a)`, derived — never authored (see [`Seam`]).
149 pub rise: i64,
150 /// Which place hosts the stair massing, on a `stair`.
151 pub stair_in: Option<NodeId>,
152 /// The crossing's declared form ([`Seam::form`]), handed to both places.
153 pub form: String,
154}
155
156/// The plan's boxes, resolved by the code the stage-4 checks judge with.
157///
158/// A box whose floor names an undeclared datum is **absent** — `DW0112` has
159/// refused it, and a place with no plane has no cells for any reader to work in.
160#[must_use]
161pub fn placed_boxes(c: &Campaign, reads: &mut Reads) -> Vec<PlacedBox> {
162 let (Some(plan), Some(graph)) = (
163 c.site_plan.as_ref().map(|p| &p.content),
164 c.layout_graph.as_ref().map(|g| &g.content),
165 ) else {
166 return Vec::new();
167 };
168 let table = Metrics::table();
169 let mut sink = Vec::new();
170 resolve(plan, graph, &table, reads, &mut sink)
171 .0
172 .into_iter()
173 .map(|p| PlacedBox {
174 node: p.plan.node.clone(),
175 foot: p.foot,
176 floor: p.floor,
177 clearance: p.clearance,
178 open: matches!(p.plan.ceiling, Ceiling::Open(_)),
179 base: p.plan.base,
180 roof: p.plan.roof,
181 })
182 .collect()
183}
184
185/// **The one place a seam's crossing rectangle is computed**, for either kind
186/// of connection (spec-0053 §4).
187///
188/// A portal's rectangle is its opening's `width × height` — the named
189/// standard's or the declared one — anchored at `at`. A contact's is its span: `at` plus the declared `extent`, or `at` to
190/// the far edge of the shared face when no extent is declared.
191///
192/// One function rather than one per kind, and one call rather than a copy in
193/// each reader, because this rectangle is simultaneously the derivation's carve,
194/// `DW0836`'s allocation, `DW0838`'s allocation set and `DW0877`'s span. Two
195/// implementations of it would be a plan-time green and a byte-time green about
196/// two different rectangles, which is the defect `shared_face` already has one
197/// implementation to prevent.
198///
199/// `None` when the seam names an opening the table does not define — `DW0812`
200/// refused it and there is no rectangle to build or measure.
201pub(super) fn crossing_rect(
202 s: &Seam,
203 at: [i64; 2],
204 face: &SharedFace,
205 table: &Metrics,
206 reads: &mut Reads,
207) -> Option<(Crossing, [i64; 2])> {
208 if s.contact.is_some() {
209 return Some((Crossing::Contact, contact_extent(s, at, face)));
210 }
211 let o = s.opening.as_ref()?.resolve(table, reads).ok()?;
212 Some((Crossing::Portal, [i64::from(o.width), i64::from(o.height)]))
213}
214
215/// **How big a contact's span is** — the one authority, read by
216/// [`crossing_rect`] and by the refusal that judges it (`DW0876`).
217///
218/// A declared `extent` is taken as written. With none declared the span runs
219/// from `at` to the far edge of the shared face on both axes, which is how a
220/// contact along the whole of a face is spelled. An `at` already past that edge
221/// would give a negative extent, so it is clamped to one cell: the rectangle
222/// stays well-formed and `DW0876` describes it, rather than the arithmetic
223/// producing a rectangle nothing downstream could reason about.
224pub(super) fn contact_extent(s: &Seam, at: [i64; 2], face: &SharedFace) -> [i64; 2] {
225 match s.contact.as_ref().and_then(|c| c.extent) {
226 Some(e) => [i64::from(e[0].get()), i64::from(e[1].get())],
227 None => [(face.u.1 - at[0] + 1).max(1), (face.v.1 - at[1] + 1).max(1)],
228 }
229}
230
231/// Which axis a face is flat in: 0 = x, 1 = y, 2 = z.
232#[must_use]
233pub fn normal_axis_of(face: Face) -> usize {
234 match face {
235 Face::East | Face::West => 0,
236 Face::Up | Face::Down => 1,
237 Face::South | Face::North => 2,
238 }
239}
240
241/// A seam's crossing rectangle as an inclusive world AABB, flat in the face's
242/// normal axis — [`crossing_rect`]'s two numbers put where the world is.
243///
244/// Extracted rather than written twice because [`stair_run`] needs the same
245/// rectangle at validation tier, before any `PlacedSeam` exists.
246pub(super) fn crossing_aabb(
247 s: &Seam,
248 at: [i64; 2],
249 face: &SharedFace,
250 extent: [i64; 2],
251) -> ([i64; 3], [i64; 3]) {
252 let normal_axis = normal_axis_of(s.face);
253 let (u_axis, v_axis) = in_plane_axes(s.face);
254 let mut lo = [0i64; 3];
255 let mut hi = [0i64; 3];
256 lo[normal_axis] = face.plane;
257 hi[normal_axis] = face.plane;
258 lo[u_axis] = at[0];
259 hi[u_axis] = at[0] + extent[0] - 1;
260 lo[v_axis] = at[1];
261 hi[v_axis] = at[1] + extent[1] - 1;
262 (lo, hi)
263}
264
265/// **What a stair costs the box that hosts it** — the one place the geometry of
266/// a run is worked out, for both the check that refuses a plan (`DW0830`) and
267/// the derivation that lays the treads.
268///
269/// # Why this is one function and not two
270///
271/// It was two, and the two disagreed. `DW0830` measured the run against the
272/// seam's **rise** and against the host's whole **extent**; the derivation
273/// measures it against the **climb** the opening really asks for and against
274/// the run the host really has beside the hole. Both readings are defensible
275/// in isolation and neither is the other, so a plan could pass the check by one
276/// arithmetic and be refused by the other at build time — the treads then went
277/// unlaid, and the place they were the only way into came back as `DW0837` with
278/// nothing pointing at the stair. Found by building: a two-place plan whose
279/// lower room is entered through a hole in the upper room's floor reached green
280/// at stage 4 with `needs 8, affords 8` and built no stair at all, because the
281/// climb to the pierced plane is 7 and the run beside a centred hole is 6.
282///
283/// So the rule lives here and both readers call it. A plan that reaches green
284/// is a plan the derivation can build, by construction rather than by two
285/// arithmetics agreeing.
286///
287/// # What the numbers mean
288///
289/// * `climb` — how high the courses must carry a body. Across a **vertical**
290/// face that is the seam's own sill, because the body stands at the sill and
291/// steps through; through a **floor or ceiling** it is the pierced plane,
292/// because the body stands in the hole and steps out beside it. Both come out
293/// as the floor difference on an ordinary plan and differ exactly where the
294/// plan puts the sill somewhere other than the far floor.
295/// * `run_axis` — the horizontal axis the run is spent on. Across a vertical
296/// face it is that face's normal; through a floor or ceiling the run may go
297/// either way, so it is the host's longer horizontal axis.
298/// * `start`/`step` — the stair arrives AT its seam, so course 0 is the one the
299/// body steps off and the run walks back into the room.
300/// * `available` — how many cells of that walk the host really has. Across a
301/// vertical face the run walks the whole footprint. Through a floor or a
302/// ceiling it leaves along **one** side of the hole, so it is the room on the
303/// roomier side plus the hole's own width — never the host's whole extent,
304/// which is a run that only exists if the stair could run both ways at once.
305///
306/// `None` where there is no run to lay or judge: a host at or above what the
307/// stair has to reach (the plan named the higher place, which `DW0830` refuses
308/// by name), or a hole that is not over this host at all (`DW0828`'s finding).
309#[must_use]
310pub fn stair_run(
311 host_floor: i64,
312 host_foot: [i64; 4],
313 normal_axis: usize,
314 plane: i64,
315 opening: ([i64; 3], [i64; 3]),
316) -> Option<StairRun> {
317 let (olo, ohi) = opening;
318 let lo = [host_foot[0], host_floor, host_foot[2]];
319 let hi = [host_foot[1], host_floor, host_foot[3]];
320 let target = if normal_axis == 1 { plane } else { olo[1] };
321 let climb = target - host_floor;
322 if climb <= 0 {
323 return None;
324 }
325 let run_axis = if normal_axis == 1 {
326 let ex = host_foot[1] - host_foot[0] + 1;
327 let ez = host_foot[3] - host_foot[2] + 1;
328 if ex >= ez { 0usize } else { 2 }
329 } else {
330 normal_axis
331 };
332 let (start, step, available) = if normal_axis == 1 {
333 let (olo_r, ohi_r) = (
334 olo[run_axis].max(lo[run_axis]),
335 ohi[run_axis].min(hi[run_axis]),
336 );
337 if olo_r > ohi_r {
338 return None;
339 }
340 let width = ohi_r - olo_r + 1;
341 let room_lo = olo_r - lo[run_axis];
342 let room_hi = hi[run_axis] - ohi_r;
343 if room_lo >= room_hi {
344 (ohi_r, -1, room_lo + width)
345 } else {
346 (olo_r, 1, room_hi + width)
347 }
348 } else if plane > hi[run_axis] {
349 (hi[run_axis], -1, hi[run_axis] - lo[run_axis] + 1)
350 } else {
351 (lo[run_axis], 1, hi[run_axis] - lo[run_axis] + 1)
352 };
353 Some(StairRun {
354 climb,
355 run_axis,
356 start,
357 step,
358 available,
359 })
360}
361
362/// What a stair's treads cost their host — see [`stair_run`].
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub struct StairRun {
365 /// How high the courses must carry a body, off the host's own walk plane.
366 pub climb: i64,
367 /// The horizontal axis the run walks: 0 = x, 2 = z.
368 pub run_axis: usize,
369 /// Where course 0 — the one the body steps off — stands on `run_axis`.
370 pub start: i64,
371 /// Which way the run walks back into the host: `+1` or `-1`.
372 pub step: i64,
373 /// How many cells of that walk the host really affords.
374 pub available: i64,
375}
376
377/// The run one standard pitch costs a climb, rounded up: a course is a whole
378/// cell, and a run two-thirds of a cell short is a run the host does not have.
379#[must_use]
380pub fn run_of(pitch: &Pitch, climb: i64) -> i64 {
381 let (span, per) = (climb.abs() * i64::from(pitch.run), i64::from(pitch.rise));
382 if per == 0 {
383 span
384 } else {
385 span / per + i64::from(span % per != 0)
386 }
387}
388
389/// **The gentlest standard pitch a climb fits in `available` cells of run**, or
390/// `None` when the table defines none that does.
391///
392/// The walk is over [`Metrics::names_of`] in table order — gentlest first — and
393/// takes the first that fits. One authority, so the verdict `DW0830` reaches and
394/// the geometry the derivation lays cannot be about different standards.
395#[must_use]
396pub fn gentlest_pitch(
397 table: &Metrics,
398 reads: &mut Reads,
399 climb: i64,
400 available: i64,
401) -> Option<Pitch> {
402 for name in table.names_of(MetricKind::Pitch) {
403 let Ok(entry) = table.resolve(MetricKind::Pitch, name) else {
404 continue;
405 };
406 let MetricValue::Pitch(p) = entry.value(reads) else {
407 continue;
408 };
409 if p.rise == 0 {
410 continue;
411 }
412 if run_of(p, climb) <= available {
413 return Some(*p);
414 }
415 }
416 None
417}
418
419/// The standard pitch that costs a climb the LEAST run, and what that run is —
420/// the number `DW0830`'s refusal quotes, because it is the shortest run any
421/// standard could do the climb in.
422#[must_use]
423pub fn tightest_pitch(
424 table: &Metrics,
425 reads: &mut Reads,
426 climb: i64,
427) -> Option<(&'static str, i64)> {
428 let mut best: Option<(&'static str, i64)> = None;
429 for name in table.names_of(MetricKind::Pitch) {
430 let Ok(entry) = table.resolve(MetricKind::Pitch, name) else {
431 continue;
432 };
433 let MetricValue::Pitch(p) = entry.value(reads) else {
434 continue;
435 };
436 if p.rise == 0 {
437 continue;
438 }
439 let needed = run_of(p, climb);
440 if best.is_none_or(|(_, b)| needed < b) {
441 best = Some((name, needed));
442 }
443 }
444 best
445}
446
447/// The plan's seams, resolved by the code the stage-4 checks judge with.
448///
449/// A seam whose face the two boxes do not share, or whose opening the table does
450/// not define, is **absent**: `DW0828`/`DW0812` refused it, and there is no hole
451/// for a reader to build or measure.
452#[must_use]
453pub fn placed_seams(c: &Campaign, boxes: &[PlacedBox], reads: &mut Reads) -> Vec<PlacedSeam> {
454 let (Some(plan), Some(graph)) = (
455 c.site_plan.as_ref().map(|p| &p.content),
456 c.layout_graph.as_ref().map(|g| &g.content),
457 ) else {
458 return Vec::new();
459 };
460 let table = Metrics::table();
461 let by_node: BTreeMap<&str, &PlacedBox> =
462 boxes.iter().map(|b| (b.node.0.as_str(), b)).collect();
463 let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
464 // The seam anchors come from the same packing that placed `boxes`: one
465 // arithmetic, so the derivation and its observer cannot disagree about
466 // where a hole is.
467 let mut sink = Vec::new();
468 let (_, packed) = resolve(plan, graph, &table, reads, &mut sink);
469 let mut out = Vec::new();
470 for (i, s) in plan.seams.iter().enumerate() {
471 let Some(at) = packed.seam_at[i] else {
472 continue; // the packing refused or could not place this seam
473 };
474 let Some(edge) = edges.get(s.edge.0.as_str()) else {
475 continue;
476 };
477 if !edge.has_seam() {
478 continue;
479 }
480 let (Some(a), Some(b)) = (
481 by_node.get(edge.a().0.as_str()).copied(),
482 by_node.get(edge.b().0.as_str()).copied(),
483 ) else {
484 continue;
485 };
486 let Ok(face) = shared_face_of(a, b, s.face) else {
487 continue;
488 };
489 let Some((crossing, extent)) = crossing_rect(s, at, &face, &table, reads) else {
490 continue;
491 };
492 let normal_axis = normal_axis_of(s.face);
493 // The face's two in-plane axes, in the order `at` names them.
494 let (u_axis, v_axis) = in_plane_axes(s.face);
495 let (lo, hi) = crossing_aabb(s, at, &face, extent);
496 let mut smin = [0i64; 3];
497 let mut smax = [0i64; 3];
498 smin[normal_axis] = face.plane;
499 smax[normal_axis] = face.plane;
500 smin[u_axis] = face.u.0;
501 smax[u_axis] = face.u.1;
502 smin[v_axis] = face.v.0;
503 smax[v_axis] = face.v.1;
504 out.push(PlacedSeam {
505 edge: s.edge.clone(),
506 class: edge.class(),
507 a: edge.a().clone(),
508 b: edge.b().clone(),
509 face: s.face,
510 normal_axis,
511 plane: face.plane,
512 opening: (lo, hi),
513 shared: (smin, smax),
514 crossing,
515 rise: b.floor - a.floor,
516 stair_in: s.stair_in.clone(),
517 form: s.form.clone(),
518 });
519 }
520 out
521}
522
523/// **A contact**: the span of a shared face along which two places simply meet
524/// (spec-0053 §4).
525///
526/// # What a contact MEANS
527///
528/// The boundary is continuous ground. The derivation writes **no wall along the
529/// span** — and wall as ever outside it — and crossing is legitimate anywhere
530/// along it the step rule admits. It is not a wide door: `DW0829`'s standard-name
531/// resolution and sill rule are portal checks and do not apply, because a
532/// contact has no opening name to resolve and no single sill. Calling a 55-cell
533/// front a door would make every downstream door check wrong.
534///
535/// # What the author allocates and what the engine measures
536///
537/// The author allocates **where** the places meet. The engine measures the
538/// **crossing profile** from assembled bytes — which columns of the span a body
539/// actually crosses under the step rule — and `DW0877` refuses a contact nothing
540/// can cross. *"This face is fine"* is never a declaration this engine accepts.
541///
542/// Seams stay **allocated, never discovered**: the span is the edge's allocation
543/// set for `DW0838`, so a crossing outside it is still a refusal.
544#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
545#[serde(deny_unknown_fields)]
546pub struct Contact {
547 /// The span, `[u, v]` in cells on the face's own two in-plane axes, anchored
548 /// at the seam's `at`.
549 ///
550 /// **Omitted, the span runs from `at` to the far edge of the shared face on
551 /// both axes** — which is how a contact along the whole of a face is
552 /// written, by putting `at` at the face's low corner. `DW0828`'s refusal
553 /// prints that corner, so the number an author needs is in the message they
554 /// would already be reading.
555 ///
556 /// There is no `width` standard and no `length` here, and both absences are
557 /// the design (spec-0053 §7).
558 #[serde(default, skip_serializing_if = "Option::is_none")]
559 pub extent: Option<[NonZeroU32; 2]>,
560}
561
562/// **Which kind of connection a seam allocates** (spec-0053 §4).
563///
564/// Carried on the resolved seam rather than re-derived from the authored one at
565/// each reader, so that the derivation and the byte observer cannot disagree
566/// about which kind a seam is — the same reason `shared_face` has one
567/// implementation.
568#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
569#[serde(rename_all = "kebab-case")]
570pub enum Crossing {
571 /// A standard opening. Every allocated cell must be passable (`DW0836`).
572 Portal,
573 /// A front where two places meet. No wall along the span, and **at least
574 /// one** passable column of body width somewhere in it (`DW0877`) — not
575 /// every cell, because a contact is ground rather than a hole and the
576 /// massing standing on it is content.
577 Contact,
578}
579
580/// The two world axes a face's `at` names, in that order.
581fn in_plane_axes(face: Face) -> (usize, usize) {
582 match face {
583 // `[along, y]` — `z` for east/west, `x` for north/south.
584 Face::East | Face::West => (2, 1),
585 Face::South | Face::North => (0, 1),
586 // `[x, z]`.
587 Face::Up | Face::Down => (0, 2),
588 }
589}
590
591/// A footprint's inclusive span on one WORLD axis (0 = x, 2 = z). Axis 1 has no
592/// answer here — a footprint is horizontal — and no caller asks for it.
593pub(super) fn span(foot: [i64; 4], axis: usize) -> (i64, i64) {
594 if axis == 0 {
595 (foot[0], foot[1])
596 } else {
597 (foot[2], foot[3])
598 }
599}
600
601/// Inclusive overlap of two ranges, or `None`.
602pub(super) fn overlap(a: (i64, i64), b: (i64, i64)) -> Option<(i64, i64)> {
603 let lo = a.0.max(b.0);
604 let hi = a.1.min(b.1);
605 (lo <= hi).then_some((lo, hi))
606}
607
608/// Is `[lo, hi]` inside `[within_lo, within_hi]`?
609pub(super) fn within(r: (i64, i64), w: (i64, i64)) -> bool {
610 r.0 >= w.0 && r.1 <= w.1
611}
612
613/// The region's inclusive span on one axis.
614pub(super) fn region_span(region: &WorldBox, axis: usize) -> (i64, i64) {
615 (region.min[axis], region.max()[axis])
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 /// **The run beside a pierced floor is not the host's extent**, and this is
623 /// the arithmetic the whole one-authority repair turns on.
624 ///
625 /// An eight-by-eight host with a three-wide hole cut through the ceiling
626 /// above it: the treads leave along ONE side of the hole, so what they have
627 /// is the room on the roomier side plus the hole's own width. Centre the
628 /// hole and that is 3 + 3 = 6, not 8 — and the courses have to carry the
629 /// body to the pierced plane, which is one below the floor of the place
630 /// above it.
631 ///
632 /// The numbers are read off the geometry by hand rather than recomputed
633 /// here, so a change to the rule shows up as a failure rather than as two
634 /// arithmetics agreeing.
635 #[test]
636 fn a_run_beside_a_pierced_floor_is_the_room_on_one_side_plus_the_hole() {
637 // Host x 4..11, z 4..11, walk plane y 56; the hole is cut at y 63 and
638 // spans x 7..9 — three of the eight cells, three to the low side and
639 // two to the high.
640 let run = stair_run(56, [4, 11, 4, 11], 1, 63, ([7, 63, 6], [9, 63, 8]))
641 .expect("the hole is over this host and the plane is above its floor");
642 assert_eq!(run.climb, 7, "56 up to the pierced plane at 63");
643 assert_eq!(run.run_axis, 0, "the host is square, so the run takes x");
644 assert_eq!(
645 run.available, 6,
646 "three cells of room on the low side, plus the hole's three"
647 );
648 assert_eq!(
649 run.start, 9,
650 "course 0 stands under the far edge of the hole"
651 );
652 assert_eq!(run.step, -1, "and the run walks back into the room");
653 }
654
655 /// The same host, the same hole, and the check and the derivation asking the
656 /// same question: seven courses do not fit six cells, so no standard pitch
657 /// does either. Measured against the host's extent it would — which is the
658 /// green a plan used to reach before building nothing.
659 #[test]
660 fn no_standard_pitch_fits_a_climb_of_seven_in_six_cells_of_run() {
661 let table = Metrics::table();
662 let mut reads = Reads::default();
663 assert!(gentlest_pitch(&table, &mut reads, 7, 6).is_none());
664 assert!(gentlest_pitch(&table, &mut reads, 7, 7).is_some());
665 assert_eq!(tightest_pitch(&table, &mut reads, 7), Some(("stair", 7)));
666 }
667
668 /// Across a vertical face the run walks the whole footprint and the climb is
669 /// the seam's own sill — the body stands at the sill and steps through.
670 #[test]
671 fn a_run_across_a_vertical_face_is_the_whole_footprint() {
672 // Host x 4..11, z 4..11, walk plane y 64; the wall is at x 12 and the
673 // opening's low corner is at y 72.
674 let run = stair_run(64, [4, 11, 4, 11], 0, 12, ([12, 72, 6], [12, 74, 8]))
675 .expect("the wall is beyond the host, so the run walks back from it");
676 assert_eq!(run.climb, 8, "64 up to the sill at 72");
677 assert_eq!(run.run_axis, 0);
678 assert_eq!(run.available, 8, "the host's whole extent on x");
679 assert_eq!(run.start, 11, "course 0 stands against the wall");
680 assert_eq!(run.step, -1);
681 }
682}