use std::collections::{BTreeMap, BTreeSet};
use std::num::NonZeroU32;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::diagnostic::{Diagnostic, DwCode, ExitTier};
use crate::envelope::Campaign;
use crate::ids::{DatumId, EdgeId, FactId, NodeId, ViewId, VolumeId};
use crate::layout::{Edge, LayoutGraphContent, StationKind};
use crate::metrics::{
MAX_JUMP_RISE_16, MetricKind, MetricValue, Metrics, Pitch, Reads, SizeClass, WayClass,
passable_clearance_cells, passable_width_cells,
};
use crate::stages::AreaLighting;
pub const DW_PLAN_AGREEMENT: DwCode = DwCode::new("DW0824", ExitTier::Build);
pub const DW_BOX_OFF_GRID: DwCode = DwCode::new("DW0825", ExitTier::Build);
pub const DW_BOX_LEAVES_REGION: DwCode = DwCode::new("DW0826", ExitTier::Build);
pub const DW_BOXES_OVERLAP: DwCode = DwCode::new("DW0827", ExitTier::Build);
pub const DW_SEAM_NOT_SHARED: DwCode = DwCode::new("DW0828", ExitTier::Build);
pub const SHARED_FACE_GAP_CELLS: i64 = 1;
pub const DW_SEAM_OPENING: DwCode = DwCode::new("DW0829", ExitTier::Build);
pub const DW_STAIR_PITCH: DwCode = DwCode::new("DW0830", ExitTier::Build);
pub const DW_DROP_POLICY: DwCode = DwCode::new("DW0831", ExitTier::Build);
pub const DW_CONTACT: DwCode = DwCode::new("DW0876", ExitTier::Build);
pub const DW_SIZE_CLASS: DwCode = DwCode::new("DW0832", ExitTier::Build);
pub const DW_IDENTITY_FALSE: DwCode = DwCode::new("DW0833", ExitTier::Build);
pub const DW_IDENTITY_EMPTY: DwCode = DwCode::new("DW0834", ExitTier::Build);
pub const DW_VOLUME_IN_BOX: DwCode = DwCode::new("DW0835", ExitTier::Build);
pub const DW_TWO_AUTHORITIES: DwCode = DwCode::new("DW0839", ExitTier::Build);
pub const DW_UNPLACED: DwCode = DwCode::new("DW0883", ExitTier::Build);
pub const SITE_AREA: &str = "area/site";
pub const ENTRY_ANCHOR: &str = "spawn";
#[must_use]
pub fn node_anchor(node: &NodeId) -> String {
format!("anchor/node-{}", slug(node.0.as_str()))
}
#[must_use]
pub fn seam_anchor(edge: &EdgeId) -> String {
format!("anchor/seam-{}", slug(edge.0.as_str()))
}
#[must_use]
pub fn seam_unlock_anchor(edge: &EdgeId) -> String {
format!("anchor/unlock-{}", slug(edge.0.as_str()))
}
fn slug(id: &str) -> &str {
id.split_once('/').map_or(id, |(_, rest)| rest)
}
pub const SEAM_BAR: &str = "minecraft:iron_bars";
#[must_use]
pub fn synthesized_gate_block(c: &Campaign, anchor: &str) -> Option<&'static str> {
matches!(
synthesized_anchor_kinds(c).get(anchor),
Some(StationKind::Gate)
)
.then_some(SEAM_BAR)
}
#[must_use]
pub fn synthesized_anchor_kinds(c: &Campaign) -> BTreeMap<String, StationKind> {
let mut out: BTreeMap<String, StationKind> = BTreeMap::new();
if c.site_plan.is_none() {
return out;
}
let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
return out; };
out.insert(ENTRY_ANCHOR.to_string(), StationKind::Point);
for n in &graph.nodes {
out.insert(node_anchor(&n.id), StationKind::Point);
for s in &n.stations {
out.insert(s.anchor.as_str().to_string(), s.kind);
}
}
for e in &graph.edges {
let Edge::Barred { id, opens_from, .. } = e else {
continue;
};
out.insert(seam_anchor(id), StationKind::Gate);
if !matches!(opens_from, crate::layout::OpensFrom::Either) {
out.insert(seam_unlock_anchor(id), StationKind::Point);
}
}
out
}
#[must_use]
pub fn synthesized_anchors(c: &Campaign) -> BTreeSet<String> {
synthesized_anchor_kinds(c).into_keys().collect()
}
#[must_use]
pub fn owed_anchors(c: &Campaign, node: &NodeId) -> BTreeSet<String> {
let mut out: BTreeSet<String> = BTreeSet::new();
if c.site_plan.is_none() {
return out;
}
let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
return out; };
let Some(n) = graph.nodes.iter().find(|n| &n.id == node) else {
return out; };
out.insert(node_anchor(node));
if &graph.entry == node {
out.insert(ENTRY_ANCHOR.to_string());
}
for s in &n.stations {
out.insert(s.anchor.as_str().to_string());
}
for e in &graph.edges {
let Edge::Barred { id, opens_from, .. } = e else {
continue;
};
let side = match opens_from {
crate::layout::OpensFrom::A => e.a(),
crate::layout::OpensFrom::B => e.b(),
crate::layout::OpensFrom::Either => continue,
};
if side == node {
out.insert(seam_unlock_anchor(id));
}
}
out
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SitePlanContent {
pub region: WorldBox,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub datums: Vec<Datum>,
pub boxes: Vec<PlanBox>,
pub seams: Vec<Seam>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub volumes: Vec<Volume>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub identities: Vec<Identity>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sightlines: Vec<Sightline>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub views: Vec<View>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lighting: Option<AreaLighting>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorldBox {
pub min: [i64; 3],
pub extent: [NonZeroU32; 3],
}
impl WorldBox {
#[must_use]
pub fn max(&self) -> [i64; 3] {
[
self.min[0] + i64::from(self.extent[0].get()) - 1,
self.min[1] + i64::from(self.extent[1].get()) - 1,
self.min[2] + i64::from(self.extent[2].get()) - 1,
]
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Datum {
pub id: DatumId,
pub y: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum Floor {
Datum(DatumId),
Y(i64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum Ceiling {
Clearance(NonZeroU32),
Open,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PlanBox {
pub node: NodeId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<[i64; 2]>,
pub extent: [NonZeroU32; 2],
pub floor: Floor,
pub ceiling: Ceiling,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum Face {
East,
West,
Up,
Down,
South,
North,
}
impl Face {
#[must_use]
pub fn vector(self) -> [i64; 3] {
match self {
Face::East => [1, 0, 0],
Face::West => [-1, 0, 0],
Face::Up => [0, 1, 0],
Face::Down => [0, -1, 0],
Face::South => [0, 0, 1],
Face::North => [0, 0, -1],
}
}
#[must_use]
pub fn is_horizontal_plane(self) -> bool {
matches!(self, Face::Up | Face::Down)
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Face::East => "east",
Face::West => "west",
Face::Up => "up",
Face::Down => "down",
Face::South => "south",
Face::North => "north",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Offset {
Along(i64),
Plane([i64; 2]),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Seam {
pub edge: EdgeId,
pub face: Face,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub at: Option<Offset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meets: Option<Offset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opening: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stair_in: Option<NodeId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Volume {
pub id: VolumeId,
pub region: WorldBox,
pub role: VolumeRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum VolumeRole {
Massif,
Ground,
Clearance,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Identity {
pub fact: FactId,
pub measure: Measure,
pub cmp: Cmp,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "of", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Measure {
RegionExtent {
axis: Axis,
},
BoxExtent {
node: NodeId,
axis: PlanAxis,
},
BoxHeight {
node: NodeId,
},
DistanceXz {
from: NodeId,
to: NodeId,
},
DatumY {
datum: DatumId,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Axis {
X,
Y,
Z,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum PlanAxis {
X,
Z,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Cmp {
Eq,
Lt,
Le,
Gt,
Ge,
}
impl Cmp {
fn holds(self, measured: f64, fact: f64) -> bool {
match self {
Cmp::Eq => (measured - fact).abs() < 1e-9,
Cmp::Lt => measured < fact,
Cmp::Le => measured <= fact,
Cmp::Gt => measured > fact,
Cmp::Ge => measured >= fact,
}
}
fn as_str(self) -> &'static str {
match self {
Cmp::Eq => "exactly",
Cmp::Lt => "under",
Cmp::Le => "at most",
Cmp::Gt => "over",
Cmp::Ge => "at least",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Sightline {
pub edge: EdgeId,
pub from: [i64; 3],
pub to: [i64; 3],
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct View {
pub id: ViewId,
pub eye: [i64; 3],
pub look_at: [i64; 3],
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Debug, Clone)]
struct Placed<'a> {
index: usize,
plan: &'a PlanBox,
foot: [i64; 4],
floor: i64,
clearance: Option<u32>,
class: Option<PlaceClass>,
by: Provenance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PlaceClass {
Size(SizeClass),
Way(WayClass),
}
impl PlaceClass {
fn min_clearance(self) -> u32 {
match self {
PlaceClass::Size(c) => c.min_clearance,
PlaceClass::Way(w) => w.min_clearance,
}
}
}
impl Placed<'_> {
fn x0(&self) -> i64 {
self.foot[0]
}
fn x1(&self) -> i64 {
self.foot[1]
}
fn z0(&self) -> i64 {
self.foot[2]
}
fn z1(&self) -> i64 {
self.foot[3]
}
fn y_span(&self) -> Option<(i64, i64)> {
let c = i64::from(self.clearance?);
Some((self.floor, self.floor + c - 1))
}
fn centre_xz(&self) -> (f64, f64) {
(
(self.x0() as f64 + self.x1() as f64) / 2.0,
(self.z0() as f64 + self.z1() as f64) / 2.0,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlacedBox {
pub node: NodeId,
pub foot: [i64; 4],
pub floor: i64,
pub clearance: u32,
pub open: bool,
}
impl PlacedBox {
#[must_use]
pub fn space(&self) -> ([i64; 3], [i64; 3]) {
(
[self.foot[0], self.floor, self.foot[2]],
[
self.foot[1],
self.floor + i64::from(self.clearance) - 1,
self.foot[3],
],
)
}
#[must_use]
pub fn centre(&self) -> [i64; 3] {
[
(self.foot[0] + self.foot[1]) / 2,
self.floor,
(self.foot[2] + self.foot[3]) / 2,
]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlacedSeam {
pub edge: EdgeId,
pub class: &'static str,
pub a: NodeId,
pub b: NodeId,
pub face: Face,
pub normal_axis: usize,
pub plane: i64,
pub opening: ([i64; 3], [i64; 3]),
pub shared: ([i64; 3], [i64; 3]),
pub crossing: Crossing,
pub rise: i64,
pub stair_in: Option<NodeId>,
}
#[must_use]
pub fn placed_boxes(c: &Campaign, reads: &mut Reads) -> Vec<PlacedBox> {
let (Some(plan), Some(graph)) = (
c.site_plan.as_ref().map(|p| &p.content),
c.layout_graph.as_ref().map(|g| &g.content),
) else {
return Vec::new();
};
let table = Metrics::table();
let mut sink = Vec::new();
resolve(plan, graph, &table, reads, &mut sink)
.0
.into_iter()
.filter_map(|p| {
Some(PlacedBox {
node: p.plan.node.clone(),
foot: p.foot,
floor: p.floor,
clearance: p.clearance?,
open: matches!(p.plan.ceiling, Ceiling::Open),
})
})
.collect()
}
fn crossing_rect(
s: &Seam,
at: [i64; 2],
face: &SharedFace,
table: &Metrics,
reads: &mut Reads,
) -> Option<(Crossing, [i64; 2])> {
if s.contact.is_some() {
return Some((Crossing::Contact, contact_extent(s, at, face)));
}
let named = s.opening.as_ref()?;
let entry = table.resolve(MetricKind::Opening, named).ok()?;
match entry.value(reads) {
MetricValue::Opening(o) => {
Some((Crossing::Portal, [i64::from(o.width), i64::from(o.height)]))
}
_ => None,
}
}
fn contact_extent(s: &Seam, at: [i64; 2], face: &SharedFace) -> [i64; 2] {
match s.contact.as_ref().and_then(|c| c.extent) {
Some(e) => [i64::from(e[0].get()), i64::from(e[1].get())],
None => [(face.u.1 - at[0] + 1).max(1), (face.v.1 - at[1] + 1).max(1)],
}
}
#[must_use]
pub fn normal_axis_of(face: Face) -> usize {
match face {
Face::East | Face::West => 0,
Face::Up | Face::Down => 1,
Face::South | Face::North => 2,
}
}
fn crossing_aabb(
s: &Seam,
at: [i64; 2],
face: &SharedFace,
extent: [i64; 2],
) -> ([i64; 3], [i64; 3]) {
let normal_axis = normal_axis_of(s.face);
let (u_axis, v_axis) = in_plane_axes(s.face);
let mut lo = [0i64; 3];
let mut hi = [0i64; 3];
lo[normal_axis] = face.plane;
hi[normal_axis] = face.plane;
lo[u_axis] = at[0];
hi[u_axis] = at[0] + extent[0] - 1;
lo[v_axis] = at[1];
hi[v_axis] = at[1] + extent[1] - 1;
(lo, hi)
}
#[must_use]
pub fn stair_run(
host_floor: i64,
host_foot: [i64; 4],
normal_axis: usize,
plane: i64,
opening: ([i64; 3], [i64; 3]),
) -> Option<StairRun> {
let (olo, ohi) = opening;
let lo = [host_foot[0], host_floor, host_foot[2]];
let hi = [host_foot[1], host_floor, host_foot[3]];
let target = if normal_axis == 1 { plane } else { olo[1] };
let climb = target - host_floor;
if climb <= 0 {
return None;
}
let run_axis = if normal_axis == 1 {
let ex = host_foot[1] - host_foot[0] + 1;
let ez = host_foot[3] - host_foot[2] + 1;
if ex >= ez { 0usize } else { 2 }
} else {
normal_axis
};
let (start, step, available) = if normal_axis == 1 {
let (olo_r, ohi_r) = (
olo[run_axis].max(lo[run_axis]),
ohi[run_axis].min(hi[run_axis]),
);
if olo_r > ohi_r {
return None;
}
let width = ohi_r - olo_r + 1;
let room_lo = olo_r - lo[run_axis];
let room_hi = hi[run_axis] - ohi_r;
if room_lo >= room_hi {
(ohi_r, -1, room_lo + width)
} else {
(olo_r, 1, room_hi + width)
}
} else if plane > hi[run_axis] {
(hi[run_axis], -1, hi[run_axis] - lo[run_axis] + 1)
} else {
(lo[run_axis], 1, hi[run_axis] - lo[run_axis] + 1)
};
Some(StairRun {
climb,
run_axis,
start,
step,
available,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StairRun {
pub climb: i64,
pub run_axis: usize,
pub start: i64,
pub step: i64,
pub available: i64,
}
#[must_use]
pub fn run_of(pitch: &Pitch, climb: i64) -> i64 {
let (span, per) = (climb.abs() * i64::from(pitch.run), i64::from(pitch.rise));
if per == 0 {
span
} else {
span / per + i64::from(span % per != 0)
}
}
#[must_use]
pub fn gentlest_pitch(
table: &Metrics,
reads: &mut Reads,
climb: i64,
available: i64,
) -> Option<Pitch> {
for name in table.names_of(MetricKind::Pitch) {
let Ok(entry) = table.resolve(MetricKind::Pitch, name) else {
continue;
};
let MetricValue::Pitch(p) = entry.value(reads) else {
continue;
};
if p.rise == 0 {
continue;
}
if run_of(p, climb) <= available {
return Some(*p);
}
}
None
}
#[must_use]
pub fn tightest_pitch(
table: &Metrics,
reads: &mut Reads,
climb: i64,
) -> Option<(&'static str, i64)> {
let mut best: Option<(&'static str, i64)> = None;
for name in table.names_of(MetricKind::Pitch) {
let Ok(entry) = table.resolve(MetricKind::Pitch, name) else {
continue;
};
let MetricValue::Pitch(p) = entry.value(reads) else {
continue;
};
if p.rise == 0 {
continue;
}
let needed = run_of(p, climb);
if best.is_none_or(|(_, b)| needed < b) {
best = Some((name, needed));
}
}
best
}
#[must_use]
pub fn placed_seams(c: &Campaign, boxes: &[PlacedBox], reads: &mut Reads) -> Vec<PlacedSeam> {
let (Some(plan), Some(graph)) = (
c.site_plan.as_ref().map(|p| &p.content),
c.layout_graph.as_ref().map(|g| &g.content),
) else {
return Vec::new();
};
let table = Metrics::table();
let by_node: BTreeMap<&str, &PlacedBox> =
boxes.iter().map(|b| (b.node.0.as_str(), b)).collect();
let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
let mut sink = Vec::new();
let (_, packed) = resolve(plan, graph, &table, reads, &mut sink);
let mut out = Vec::new();
for (i, s) in plan.seams.iter().enumerate() {
let Some(at) = packed.seam_at[i] else {
continue; };
let Some(edge) = edges.get(s.edge.0.as_str()) else {
continue;
};
if matches!(edge, Edge::Vision { .. }) {
continue;
}
let (Some(a), Some(b)) = (
by_node.get(edge.a().0.as_str()).copied(),
by_node.get(edge.b().0.as_str()).copied(),
) else {
continue;
};
let Ok(face) = shared_face_of(a, b, s.face) else {
continue;
};
let Some((crossing, extent)) = crossing_rect(s, at, &face, &table, reads) else {
continue;
};
let normal_axis = normal_axis_of(s.face);
let (u_axis, v_axis) = in_plane_axes(s.face);
let (lo, hi) = crossing_aabb(s, at, &face, extent);
let mut smin = [0i64; 3];
let mut smax = [0i64; 3];
smin[normal_axis] = face.plane;
smax[normal_axis] = face.plane;
smin[u_axis] = face.u.0;
smax[u_axis] = face.u.1;
smin[v_axis] = face.v.0;
smax[v_axis] = face.v.1;
out.push(PlacedSeam {
edge: s.edge.clone(),
class: edge.class(),
a: edge.a().clone(),
b: edge.b().clone(),
face: s.face,
normal_axis,
plane: face.plane,
opening: (lo, hi),
shared: (smin, smax),
crossing,
rise: b.floor - a.floor,
stair_in: s.stair_in.clone(),
});
}
out
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Contact {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extent: Option<[NonZeroU32; 2]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Crossing {
Portal,
Contact,
}
fn in_plane_axes(face: Face) -> (usize, usize) {
match face {
Face::East | Face::West => (2, 1),
Face::South | Face::North => (0, 1),
Face::Up | Face::Down => (0, 2),
}
}
fn span(foot: [i64; 4], axis: usize) -> (i64, i64) {
if axis == 0 {
(foot[0], foot[1])
} else {
(foot[2], foot[3])
}
}
fn overlap(a: (i64, i64), b: (i64, i64)) -> Option<(i64, i64)> {
let lo = a.0.max(b.0);
let hi = a.1.min(b.1);
(lo <= hi).then_some((lo, hi))
}
fn within(r: (i64, i64), w: (i64, i64)) -> bool {
r.0 >= w.0 && r.1 <= w.1
}
fn region_span(region: &WorldBox, axis: usize) -> (i64, i64) {
(region.min[axis], region.max()[axis])
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
pub struct PlanBinding {
pub boxes: usize,
pub box_pairs: usize,
pub seams: usize,
pub stair_seams: usize,
pub drop_seams: usize,
pub datums: usize,
pub volumes: usize,
pub identities: usize,
pub sightlines: usize,
pub views: usize,
pub pinned: usize,
pub derived: usize,
pub components: usize,
}
impl PlanBinding {
#[must_use]
pub fn of(c: &Campaign) -> PlanBinding {
let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
return PlanBinding::default();
};
let classes: BTreeMap<&str, &'static str> = c
.layout_graph
.as_ref()
.map(|g| {
g.content
.edges
.iter()
.map(|e| (e.id().0.as_str(), e.class()))
.collect()
})
.unwrap_or_default();
let n = plan.boxes.len();
let (pinned, derived, components) = match c.layout_graph.as_ref() {
Some(g) => {
let table = Metrics::table();
let mut reads = Reads::new();
let mut sink = Vec::new();
let (_, packed) = resolve(plan, &g.content, &table, &mut reads, &mut sink);
(packed.pinned, packed.derived, packed.components)
}
None => (0, 0, 0),
};
PlanBinding {
pinned,
derived,
components,
boxes: n,
box_pairs: n * n.saturating_sub(1) / 2,
seams: plan.seams.len(),
stair_seams: plan
.seams
.iter()
.filter(|s| classes.get(s.edge.0.as_str()) == Some(&"stair"))
.count(),
drop_seams: plan
.seams
.iter()
.filter(|s| classes.get(s.edge.0.as_str()) == Some(&"drop"))
.count(),
datums: plan.datums.len(),
volumes: plan.volumes.len(),
identities: plan.identities.len(),
sightlines: plan.sightlines.len(),
views: plan.views.len(),
}
}
#[must_use]
pub fn line(&self) -> String {
format!(
"site-plan binding: {b} box(es) ({p} pair(s) compared; {pn} pinned, {dv} derived, \
in {cc} component(s)), {s} seam(s) ({st} stair, {sd} drop), {d} datum(s), {v} \
whole-owned volume(s), {i} identity(ies), {sl} sightline(s), {w} view(s).",
pn = self.pinned,
dv = self.derived,
cc = self.components,
b = self.boxes,
p = self.box_pairs,
s = self.seams,
st = self.stair_seams,
sd = self.drop_seams,
d = self.datums,
v = self.volumes,
i = self.identities,
sl = self.sightlines,
w = self.views,
)
}
}
pub fn check(c: &Campaign, reads: &mut Reads, d: &mut Vec<Diagnostic>) {
let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
return;
};
let table = Metrics::table();
ids(plan, d);
one_authority(c, d);
let brief_missing = c.geometry_brief.is_none();
if brief_missing {
d.push(Diagnostic::error(
DW_PLAN_AGREEMENT,
"site-plan",
"",
"this campaign carries a site plan and no `geometry-brief.json`. The plan is the \
embedding of a design, and the brief is where that design's numbers are written \
down; with no brief there is nothing for `identities[]` to hold the map to, and \
the region's extent is a number with no author. Write the brief first — a plan \
cannot reach green ahead of it."
.to_string(),
));
}
let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
d.push(Diagnostic::error(
DW_PLAN_AGREEMENT,
"site-plan",
"",
"this campaign carries a site plan and no `layout-graph.json`. A site plan is the \
geometric embedding OF a layout graph: every box names a place and every seam \
names a connection, so with no graph there is nothing being embedded and every \
name in this document resolves to nothing. This is the only line this state \
raises: the map's anchor vocabulary is derived from the graph too, so every \
anchor a `npcs`, `quests` or effect document names is left unjudged here rather \
than refused against an empty set, and is judged the moment the graph exists. \
Author the graph first — that ordering \
is what this refusal exists to make uncompilable rather than merely advised."
.to_string(),
));
return;
};
openers(c, graph, d);
let (placed, packed) = resolve(plan, graph, &table, reads, d);
agreement(plan, graph, &placed, d);
grid(&placed, &table, reads, d);
region(plan, &placed, d);
disjoint(&placed, d);
seams(plan, graph, &placed, &packed, &table, reads, d);
size_classes(&placed, d);
volumes_outside_boxes(plan, &placed, d);
identities(c, plan, &placed, d);
lighting(plan, d);
}
fn one_authority(c: &Campaign, d: &mut Vec<Diagnostic>) {
let n = c.world.content.areas.len();
if n == 0 {
return;
}
d.push(Diagnostic::error(
DW_TWO_AUTHORITIES,
"world",
"/content/areas",
format!(
"this campaign declares {n} `areas[]` entry(ies) AND a site plan. Those are two \
placement authorities for one world: `areas[]` seats prefab pieces on the compiler's \
fixed stride, and the site plan seats the derived blockout inside its own declared \
`region` — so every question about where something is has two answers and nothing \
says which. Keep one. A campaign that places pieces keeps `areas[]` and drops \
`site-plan.json`; a campaign whose map is the site plan declares an empty `areas` \
list and lets the plan own the space. Both surfaces are legal — what is not legal \
is one \
campaign holding both."
),
));
}
fn openers(c: &Campaign, graph: &LayoutGraphContent, d: &mut Vec<Diagnostic>) {
let mut opened: BTreeSet<&str> = BTreeSet::new();
crate::stages::for_each_campaign_effect(c, &mut |_, _, eff| {
eff.visit_deep(&mut |e| {
if let Some(a) = e.open_gate_anchor() {
opened.insert(a.0.as_str());
}
});
});
for s in &c.quests.content.shortcuts {
opened.insert(s.gate.0.as_str());
}
for (i, e) in graph.edges.iter().enumerate() {
let Edge::Barred { id, .. } = e else { continue };
let want = seam_anchor(id);
if opened.contains(want.as_str()) {
continue;
}
d.push(Diagnostic::error(
crate::layout::DW_GRAPH_MISSION,
"layout-graph",
format!("/content/edges/{i}"),
format!(
"`{id}` is barred and nothing in this campaign opens it. The derivation seals \
this seam's opening at world load and names the region `{want}`; for the way to \
ever be passable some effect has to address that name — an `open-gate` on the \
beat whose completion earns it, or a `shortcut` whose far side lifts the bar. \
The graph's `gating` says what a body must HOLD to pass, which is a different \
claim and is already checked: a way that is gated on a flag nobody spends is \
still a wall. This is the half of the obligation that could only be written once \
the region existed, so it is asked here rather than at stage 3."
),
));
}
}
fn ids(plan: &SitePlanContent, d: &mut Vec<Diagnostic>) {
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut check_id = |ok: bool, id: String, kind: &str, path: String, d: &mut Vec<Diagnostic>| {
if !ok {
d.push(Diagnostic::error(
crate::codes::ID_SYNTAX,
"site-plan",
path.clone(),
format!("malformed {kind} id `{id}` — expected `{kind}/<kebab-case>`."),
));
}
if !seen.insert(format!("{kind}:{id}")) {
d.push(Diagnostic::error(
crate::codes::ID_DUPLICATE,
"site-plan",
path,
format!(
"duplicate {kind} id `{id}` — rename one, because anything naming it would \
otherwise name both."
),
));
}
};
for (i, dat) in plan.datums.iter().enumerate() {
check_id(
dat.id.is_valid_syntax(),
dat.id.0.clone(),
"datum",
format!("/content/datums/{i}/id"),
d,
);
}
for (i, v) in plan.volumes.iter().enumerate() {
check_id(
v.id.is_valid_syntax(),
v.id.0.clone(),
"volume",
format!("/content/volumes/{i}/id"),
d,
);
}
for (i, v) in plan.views.iter().enumerate() {
check_id(
v.id.is_valid_syntax(),
v.id.0.clone(),
"view",
format!("/content/views/{i}/id"),
d,
);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Provenance {
Pinned,
Seam {
seam: usize,
edge: EdgeId,
from: NodeId,
face: Face,
},
}
impl Provenance {
fn describe(&self) -> String {
match self {
Provenance::Pinned => "pinned".to_string(),
Provenance::Seam {
edge, from, face, ..
} => format!(
"hung off `{from}` across the {face} face by the seam for `{edge}`",
face = face.as_str()
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackedBox {
pub node: NodeId,
pub min: [i64; 2],
pub by: Provenance,
}
#[derive(Debug, Default)]
struct Packed {
boxes: Vec<Option<PackedBox>>,
seam_at: Vec<Option<[i64; 2]>>,
refused: BTreeSet<usize>,
components: usize,
pinned: usize,
derived: usize,
}
#[derive(Debug, Clone, Copy)]
struct Link {
a: usize,
b: usize,
at: [i64; 2],
meets: [i64; 2],
}
fn face_axes(face: Face) -> (usize, usize) {
match face {
Face::East | Face::West => (0, 1),
Face::North | Face::South => (1, 0),
Face::Up | Face::Down => (0, 1), }
}
fn centring_width(s: &Seam, table: &Metrics, reads: &mut Reads) -> Result<Option<[i64; 2]>, ()> {
if let Some(c) = &s.contact {
return Ok(c
.extent
.map(|e| [i64::from(e[0].get()), i64::from(e[1].get())]));
}
let Some(name) = s.opening.as_deref() else {
return Err(());
};
let entry = table.resolve(MetricKind::Opening, name).map_err(|_| ())?;
match entry.value(reads) {
MetricValue::Opening(o) => Ok(Some([i64::from(o.width), i64::from(o.height)])),
_ => Err(()),
}
}
enum OffsetProblem {
Shape { which: &'static str },
OffFace {
which: &'static str,
axis: &'static str,
value: i64,
extent: i64,
width: i64,
},
}
fn offsets(
s: &Seam,
ext_a: [i64; 2],
ext_b: [i64; 2],
width: Option<[i64; 2]>,
) -> Result<([i64; 2], [i64; 2]), OffsetProblem> {
let horizontal = s.face.is_horizontal_plane();
let one = |which: &'static str,
declared: Option<Offset>,
ext: [i64; 2]|
-> Result<[i64; 2], OffsetProblem> {
let (axes, names): ([usize; 2], [&'static str; 2]) = if horizontal {
([0, 1], ["x", "z"])
} else {
let (_, along) = face_axes(s.face);
([along, 0], [if along == 0 { "x" } else { "z" }, ""])
};
let default = |i: usize| match width {
Some(w) => (ext[axes[i]] - w[i]).div_euclid(2).max(0),
None => 0,
};
let off = match (declared, horizontal) {
(None, true) => [default(0), default(1)],
(None, false) => [default(0), 0],
(Some(Offset::Plane(p)), true) => p,
(Some(Offset::Along(u)), false) => [u, 0],
_ => return Err(OffsetProblem::Shape { which }),
};
let n = if horizontal { 2 } else { 1 };
for i in 0..n {
let extent = ext[axes[i]];
let w = width.map_or(1, |w| w[i]);
if off[i] < 0 || off[i] > extent - 1 {
return Err(OffsetProblem::OffFace {
which,
axis: names[i],
value: off[i],
extent,
width: w,
});
}
}
Ok(off)
};
Ok((one("at", s.at, ext_a)?, one("meets", s.meets, ext_b)?))
}
fn derive_corner(
face: Face,
known: [i64; 2],
ext_a: [i64; 2],
ext_b: [i64; 2],
at: [i64; 2],
meets: [i64; 2],
from_a: bool,
) -> [i64; 2] {
if face.is_horizontal_plane() {
return if from_a {
[known[0] + at[0] - meets[0], known[1] + at[1] - meets[1]]
} else {
[known[0] - at[0] + meets[0], known[1] - at[1] + meets[1]]
};
}
let (normal, along) = face_axes(face);
let positive = matches!(face, Face::East | Face::South);
let mut out = [0i64; 2];
if from_a {
out[along] = known[along] + at[0] - meets[0];
out[normal] = if positive {
known[normal] + ext_a[normal] + 1
} else {
known[normal] - ext_b[normal] - 1
};
} else {
out[along] = known[along] - at[0] + meets[0];
out[normal] = if positive {
known[normal] - ext_a[normal] - 1
} else {
known[normal] + ext_b[normal] + 1
};
}
out
}
fn crossing_anchor(
face: Face,
a_min: [i64; 2],
at: [i64; 2],
floor_a: i64,
floor_b: i64,
) -> [i64; 2] {
if face.is_horizontal_plane() {
[a_min[0] + at[0], a_min[1] + at[1]]
} else {
let (_, along) = face_axes(face);
[a_min[along] + at[0], floor_a.max(floor_b)]
}
}
fn ext_of(b: &PlanBox) -> [i64; 2] {
[i64::from(b.extent[0].get()), i64::from(b.extent[1].get())]
}
fn pack(
plan: &SitePlanContent,
graph: &LayoutGraphContent,
table: &Metrics,
floors: &[Option<i64>],
reads: &mut Reads,
d: &mut Vec<Diagnostic>,
) -> Packed {
let mut out = Packed {
boxes: vec![None; plan.boxes.len()],
seam_at: vec![None; plan.seams.len()],
..Packed::default()
};
let nodes: BTreeSet<&str> = graph.nodes.iter().map(|n| n.id.0.as_str()).collect();
let mut by_node: BTreeMap<&str, usize> = BTreeMap::new();
for (i, b) in plan.boxes.iter().enumerate() {
if nodes.contains(b.node.0.as_str()) {
by_node.entry(b.node.0.as_str()).or_insert(i);
}
}
let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
for (i, b) in plan.boxes.iter().enumerate() {
if let Some(min) = b.min {
out.boxes[i] = Some(PackedBox {
node: b.node.clone(),
min,
by: Provenance::Pinned,
});
out.pinned += 1;
}
}
let mut pairs: Vec<(usize, usize)> = Vec::new();
let mut links: Vec<Option<Link>> = Vec::with_capacity(plan.seams.len());
for (i, s) in plan.seams.iter().enumerate() {
let Some(edge) = edges.get(s.edge.0.as_str()) else {
links.push(None);
continue; };
if matches!(edge, Edge::Vision { .. }) {
links.push(None);
continue; }
let (Some(&a), Some(&b)) = (
by_node.get(edge.a().0.as_str()),
by_node.get(edge.b().0.as_str()),
) else {
links.push(None);
continue; };
pairs.push((a, b));
let width = centring_width(s, table, reads).unwrap_or_default();
match offsets(s, ext_of(&plan.boxes[a]), ext_of(&plan.boxes[b]), width) {
Ok((at, meets)) => links.push(Some(Link { a, b, at, meets })),
Err(problem) => {
d.push(offset_problem(i, s, edge, problem));
out.refused.insert(i);
links.push(None);
}
}
}
loop {
let mut changed = false;
for (i, link) in links.iter().enumerate() {
let Some(l) = link else { continue };
let s = &plan.seams[i];
let (ext_a, ext_b) = (ext_of(&plan.boxes[l.a]), ext_of(&plan.boxes[l.b]));
match (out.boxes[l.a].clone(), out.boxes[l.b].clone()) {
(Some(a), None) => {
let min = derive_corner(s.face, a.min, ext_a, ext_b, l.at, l.meets, true);
out.boxes[l.b] = Some(PackedBox {
node: plan.boxes[l.b].node.clone(),
min,
by: Provenance::Seam {
seam: i,
edge: s.edge.clone(),
from: a.node,
face: s.face,
},
});
out.derived += 1;
changed = true;
}
(None, Some(b)) => {
let min = derive_corner(s.face, b.min, ext_a, ext_b, l.at, l.meets, false);
out.boxes[l.a] = Some(PackedBox {
node: plan.boxes[l.a].node.clone(),
min,
by: Provenance::Seam {
seam: i,
edge: s.edge.clone(),
from: b.node,
face: s.face,
},
});
out.derived += 1;
changed = true;
}
_ => {}
}
}
if !changed {
break;
}
}
for (i, link) in links.iter().enumerate() {
let Some(l) = link else { continue };
let (Some(a), Some(b)) = (&out.boxes[l.a], &out.boxes[l.b]) else {
continue;
};
let s = &plan.seams[i];
let (ext_a, ext_b) = (ext_of(&plan.boxes[l.a]), ext_of(&plan.boxes[l.b]));
let want = derive_corner(s.face, a.min, ext_a, ext_b, l.at, l.meets, true);
if want != b.min {
d.push(two_placements(i, s, a, b, want));
out.refused.insert(i);
continue;
}
if let (Some(fa), Some(fb)) = (floors[l.a], floors[l.b]) {
out.seam_at[i] = Some(crossing_anchor(s.face, a.min, l.at, fa, fb));
}
}
let mut parent: Vec<usize> = (0..plan.boxes.len()).collect();
fn find(p: &mut [usize], i: usize) -> usize {
let mut r = i;
while p[r] != r {
r = p[r];
}
let mut c = i;
while p[c] != r {
let n = p[c];
p[c] = r;
c = n;
}
r
}
for &(a, b) in &pairs {
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
parent[ra.max(rb)] = ra.min(rb);
}
}
let mut members: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for (i, b) in plan.boxes.iter().enumerate() {
if nodes.contains(b.node.0.as_str()) {
let r = find(&mut parent, i);
members.entry(r).or_default().push(i);
}
}
out.components = members.len();
let entry = graph.entry.0.as_str();
for (_, boxes) in members {
if boxes.iter().any(|&i| plan.boxes[i].min.is_some()) {
continue;
}
let names: Vec<String> = boxes
.iter()
.map(|&i| format!("`{}`", plan.boxes[i].node))
.collect();
let suggested = boxes
.iter()
.find(|&&i| plan.boxes[i].node.0 == entry)
.or(boxes.first())
.map(|&i| plan.boxes[i].node.to_string())
.unwrap_or_default();
d.push(Diagnostic::error(
DW_UNPLACED,
"site-plan",
format!("/content/boxes/{}", boxes[0]),
format!(
"nothing places {list}: no box among them pins its `min`, and a box stands \
only where a pin puts it or where a seam hangs it off a box that already \
stands. Pin one of them — `{suggested}` — with `\"min\": [x, z]`, and the \
seams place the rest. {count} box(es) in this component.",
list = names.join(", "),
count = boxes.len(),
),
));
}
out
}
fn offset_problem(i: usize, s: &Seam, edge: &Edge, problem: OffsetProblem) -> Diagnostic {
let (which, detail) = match problem {
OffsetProblem::Shape { which } => (
which,
if s.face.is_horizontal_plane() {
format!(
"the {face} face is a floor or ceiling with two in-plane axes, and `{which}` \
gives one number. Write `[dx, dz]` — cells along x and z from the box's \
low corner",
face = s.face.as_str()
)
} else {
format!(
"the {face} face is a wall with one horizontal axis, and `{which}` gives \
two numbers. Write one — cells along the face from the box's low corner; \
the sill is not written, it is the higher of the two floors",
face = s.face.as_str()
)
},
),
OffsetProblem::OffFace {
which,
axis,
value,
extent,
width,
} => (
which,
format!(
"`{which}` puts the crossing's corner at {value} along {axis} on a face that \
runs 0..{last} — the crossing is {width} wide and the box is {extent} on that \
axis. Write an offset on the face, or omit it and the crossing is centred; an \
offset is never quietly clamped to fit",
last = extent - 1,
),
),
};
Diagnostic::error(
DW_SEAM_NOT_SHARED,
"site-plan",
format!("/content/seams/{i}/{which}"),
format!(
"the seam for `{id}` between `{an}` and `{bn}` names no position on the {side} \
box's face: {detail}.",
id = s.edge,
an = edge.a(),
bn = edge.b(),
side = if which == "at" { "`a`" } else { "`b`" },
),
)
}
fn two_placements(i: usize, s: &Seam, a: &PackedBox, b: &PackedBox, want: [i64; 2]) -> Diagnostic {
let pinned = matches!(b.by, Provenance::Pinned);
let where_ = format!(
"`{an}` stands at [{ax}, {az}] ({a_by}); `{bn}` stands at [{bx}, {bz}] ({b_by}); hung \
off `{an}`'s {face} face by this seam, `{bn}` would stand at [{wx}, {wz}]",
an = a.node,
ax = a.min[0],
az = a.min[1],
a_by = a.by.describe(),
bn = b.node,
bx = b.min[0],
bz = b.min[1],
b_by = b.by.describe(),
face = s.face.as_str(),
wx = want[0],
wz = want[1],
);
if pinned {
Diagnostic::error(
DW_UNPLACED,
"site-plan",
format!("/content/seams/{i}"),
format!(
"two things place one box, and they disagree: {where_}. A pin is a claim the \
packing verifies, never a second authority — move the pin to the corner the \
seam derives, delete it and let the seam place the box, or change this seam's \
`at`/`meets` so the two agree.",
),
)
} else {
Diagnostic::error(
DW_SEAM_NOT_SHARED,
"site-plan",
format!("/content/seams/{i}"),
format!(
"the seam for `{id}` closes a loop, and the loop does not close: {where_}. Every \
box in the loop was placed by an earlier seam, so this one can only check; \
change its `at`/`meets` to where the two boxes really meet, or move the \
offsets of the seams that placed them.",
id = s.edge,
),
)
}
}
#[must_use]
pub fn placements(c: &Campaign) -> Vec<String> {
let (Some(plan), Some(graph)) = (
c.site_plan.as_ref().map(|p| &p.content),
c.layout_graph.as_ref().map(|g| &g.content),
) else {
return Vec::new();
};
let table = Metrics::table();
let mut reads = Reads::new();
let mut sink = Vec::new();
let (_, packed) = resolve(plan, graph, &table, &mut reads, &mut sink);
packed
.boxes
.iter()
.flatten()
.map(|b| {
format!(
"site-plan placing: `{node}` stands at [{x}, {z}] — {by}.",
node = b.node,
x = b.min[0],
z = b.min[1],
by = b.by.describe(),
)
})
.collect()
}
fn resolve<'a>(
plan: &'a SitePlanContent,
graph: &LayoutGraphContent,
table: &Metrics,
reads: &mut Reads,
d: &mut Vec<Diagnostic>,
) -> (Vec<Placed<'a>>, Packed) {
let datums: BTreeMap<&str, i64> = plan.datums.iter().map(|x| (x.id.0.as_str(), x.y)).collect();
let classes: BTreeMap<&str, (MetricKind, &str)> = graph
.nodes
.iter()
.filter_map(|n| {
let named = n
.size_class
.as_deref()
.map(|x| (MetricKind::SizeClass, x))
.or_else(|| n.way_class.as_deref().map(|x| (MetricKind::WayClass, x)))?;
Some((n.id.0.as_str(), named))
})
.collect();
let mut floors: Vec<Option<i64>> = Vec::with_capacity(plan.boxes.len());
for (i, b) in plan.boxes.iter().enumerate() {
floors.push(match &b.floor {
Floor::Y(y) => Some(*y),
Floor::Datum(id) => match datums.get(id.0.as_str()) {
Some(y) => Some(*y),
None => {
d.push(Diagnostic::error(
crate::codes::DANGLING_REF,
"site-plan",
format!("/content/boxes/{i}/floor"),
format!(
"box for `{node}` stands on `{id}`, which this plan declares no \
`datums[]` entry for. Declare the plane, or give the box its own \
`y` — a place with no plane has no walk surface, so nothing below \
can say where it is.",
node = b.node,
),
));
None
}
},
});
}
let packed = pack(plan, graph, table, &floors, reads, d);
let mut out = Vec::new();
for (i, b) in plan.boxes.iter().enumerate() {
let (Some(floor), Some(pb)) = (floors[i], &packed.boxes[i]) else {
continue; };
let class = classes
.get(b.node.0.as_str())
.and_then(|(kind, name)| table.resolve(*kind, name).ok())
.and_then(|entry| match entry.value(reads) {
MetricValue::SizeClass(sc) => Some(PlaceClass::Size(*sc)),
MetricValue::WayClass(w) => Some(PlaceClass::Way(*w)),
_ => None,
});
let clearance = match b.ceiling {
Ceiling::Clearance(c) => Some(c.get()),
Ceiling::Open => class.map(PlaceClass::min_clearance),
};
out.push(Placed {
index: i,
plan: b,
foot: [
pb.min[0],
pb.min[0] + i64::from(b.extent[0].get()) - 1,
pb.min[1],
pb.min[1] + i64::from(b.extent[1].get()) - 1,
],
floor,
clearance,
class,
by: pb.by.clone(),
});
}
(out, packed)
}
fn agreement(
plan: &SitePlanContent,
graph: &LayoutGraphContent,
placed: &[Placed<'_>],
d: &mut Vec<Diagnostic>,
) {
let fault = |path: String, msg: String, d: &mut Vec<Diagnostic>| {
d.push(Diagnostic::error(DW_PLAN_AGREEMENT, "site-plan", path, msg));
};
let nodes: BTreeSet<&str> = graph.nodes.iter().map(|n| n.id.0.as_str()).collect();
let mut boxed: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
for (i, b) in plan.boxes.iter().enumerate() {
if !nodes.contains(b.node.0.as_str()) {
fault(
format!("/content/boxes/{i}/node"),
format!(
"this box embeds `{n}`, which the layout graph declares no place for. A box \
is the geometry OF a place; one that names nothing is a room the map has \
no reason to contain. Declare the place, or delete the box.",
n = b.node,
),
d,
);
continue;
}
boxed.entry(b.node.0.as_str()).or_default().push(i);
}
for (i, n) in graph.nodes.iter().enumerate() {
match boxed.get(n.id.0.as_str()).map_or(0, Vec::len) {
1 => {}
0 => fault(
"/content/boxes".to_string(),
format!(
"place `{id}` has no box. Every place the graph declares is embedded exactly \
once — an unembedded place is a room the plan forgot, and nothing later can \
notice it, because every geometric rule quantifies over the boxes that \
exist. (Graph node {i} of {total}.)",
id = n.id,
total = graph.nodes.len(),
),
d,
),
k => fault(
"/content/boxes".to_string(),
format!(
"place `{id}` has {k} boxes. A place is one space; two boxes for it make \
every rule below pick one of them and no rule says which.",
id = n.id,
),
d,
),
}
}
let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
let mut seamed: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
for (i, s) in plan.seams.iter().enumerate() {
match edges.get(s.edge.0.as_str()) {
None => fault(
format!("/content/seams/{i}/edge"),
format!(
"this seam allocates `{e}`, which the layout graph declares no connection \
for. A seam is an opening cut for a connection; one that names nothing is a \
hole in a wall for no reason.",
e = s.edge,
),
d,
),
Some(Edge::Vision { .. }) => fault(
format!("/content/seams/{i}/edge"),
format!(
"`{e}` is a `vision` connection and carries a **sightline**, not a seam. A \
seam is an opening on a shared face, and a vista's two ends are routinely \
not adjacent — a tower seen from a shore shares no face with it — so the \
seam construct cannot state the one thing a vision connection asserts. Move \
it to `sightlines[]`.",
e = s.edge,
),
d,
),
Some(_) => {
seamed.entry(s.edge.0.as_str()).or_default().push(i);
}
}
}
let mut sighted: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
for (i, s) in plan.sightlines.iter().enumerate() {
match edges.get(s.edge.0.as_str()) {
None => fault(
format!("/content/sightlines/{i}/edge"),
format!(
"this sightline embeds `{e}`, which the layout graph declares no connection \
for.",
e = s.edge,
),
d,
),
Some(e) if !matches!(e, Edge::Vision { .. }) => fault(
format!("/content/sightlines/{i}/edge"),
format!(
"`{id}` is a `{class}` connection: a body passes along it, so it is allocated \
a **seam** on a shared face rather than a line of sight. Move it to \
`seams[]`.",
id = s.edge,
class = e.class(),
),
d,
),
Some(_) => {
sighted.entry(s.edge.0.as_str()).or_default().push(i);
}
}
}
for (i, e) in graph.edges.iter().enumerate() {
let (what, held, other) = if e.is_traversal() {
("seam", &seamed, "seams")
} else {
("sightline", &sighted, "sightlines")
};
match held.get(e.id().0.as_str()).map_or(0, Vec::len) {
1 => {}
0 => fault(
format!("/content/{other}"),
format!(
"connection `{id}` ({class}) has no {what}. **Seams are allocated, not \
discovered**: two places connect because the plan cut an opening between \
them while both were still free to move, never because a wall happened to \
be low somewhere. A connection with nothing allocated is a promise the \
geometry has not been asked to keep. (Graph edge {i} of {total}.)",
id = e.id(),
class = e.class(),
total = graph.edges.len(),
),
d,
),
k => fault(
format!("/content/{other}"),
format!(
"connection `{id}` has {k} {what}s. One connection is one way through; two \
openings for it are two ways, and the graph declared one.",
id = e.id(),
),
d,
),
}
}
let by_node: BTreeMap<&str, &Placed<'_>> =
placed.iter().map(|p| (p.plan.node.0.as_str(), p)).collect();
for (i, s) in plan.seams.iter().enumerate() {
let Some(e) = edges.get(s.edge.0.as_str()) else {
continue;
};
let Some(host) = &s.stair_in else {
continue;
};
if !matches!(e, Edge::Stair { .. }) {
fault(
format!("/content/seams/{i}/stair_in"),
format!(
"this seam declares stair massing in `{host}`, and `{id}` is a `{class}` \
connection. Only a stair is built out of treads; on anything else the \
declaration is a fact about the geometry that the graph contradicts.",
id = s.edge,
class = e.class(),
),
d,
);
} else if host != e.a() && host != e.b() {
fault(
format!("/content/seams/{i}/stair_in"),
format!(
"this seam hosts its stair in `{host}`, which is neither end of `{id}` \
(`{a}` and `{b}`). A stair stands in one of the two places it joins.",
id = s.edge,
a = e.a(),
b = e.b(),
),
d,
);
}
}
for (i, s) in plan.sightlines.iter().enumerate() {
let Some(e) = edges.get(s.edge.0.as_str()) else {
continue;
};
for (end, point, node) in [("from", s.from, e.a()), ("to", s.to, e.b())] {
let Some(p) = by_node.get(node.0.as_str()) else {
continue;
};
if contains_point(p, point) {
continue;
}
fault(
format!("/content/sightlines/{i}/{end}"),
format!(
"`{id}` is a line of sight between `{a}` and `{b}`, and its `{end}` end \
`[{x}, {y}, {z}]` is not inside `{node}`. The stage-5 proof walks exactly \
this segment and calls the result the vista's; a segment whose ends are \
somewhere else would be proving a different claim, green or red.",
id = s.edge,
a = e.a(),
b = e.b(),
x = point[0],
y = point[1],
z = point[2],
),
d,
);
}
}
}
fn contains_point(p: &Placed<'_>, at: [i64; 3]) -> bool {
if at[0] < p.x0() || at[0] > p.x1() || at[2] < p.z0() || at[2] > p.z1() {
return false;
}
match p.y_span() {
Some((lo, hi)) => at[1] >= lo && at[1] <= hi,
None => at[1] >= p.floor,
}
}
fn grid(placed: &[Placed<'_>], table: &Metrics, reads: &mut Reads, d: &mut Vec<Diagnostic>) {
let Some(grid) = table.grid(reads) else {
return; };
let q = grid.quantum;
if q == 0 {
return;
}
for p in placed {
for (axis, name) in [(0usize, "x"), (1usize, "z")] {
let e = p.plan.extent[axis].get();
if !off_grid(e, q) {
continue;
}
d.push(Diagnostic::error(
DW_BOX_OFF_GRID,
"site-plan",
format!("/content/boxes/{}/extent/{axis}", p.index),
format!(
"box for `{node}` is {e} blocks on {name}, and the kit grid's quantum is \
{q} — so it is not a multiple of it. Every box's footprint is a whole \
number of quanta on both horizontal axes, which is what lets a kit piece \
land in one without being cut. The nearest multiples are {lo} and {hi}.",
node = p.plan.node,
lo = e - e % q,
hi = e - e % q + q,
),
));
}
}
}
fn off_grid(extent: u32, quantum: u32) -> bool {
quantum != 0 && !extent.is_multiple_of(quantum)
}
#[must_use]
pub fn refused_upstream(
c: &Campaign,
node: &NodeId,
resolved: &[PlacedSeam],
reads: &mut Reads,
) -> String {
let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
return String::new();
};
let mut parts: Vec<String> = Vec::new();
if let Some(q) = Metrics::table().grid(reads).map(|g| g.quantum)
&& let Some(b) = plan.boxes.iter().find(|b| &b.node == node)
{
let axes: Vec<&str> = [(0usize, "x"), (1usize, "z")]
.into_iter()
.filter(|(a, _)| off_grid(b.extent[*a].get(), q))
.map(|(_, name)| name)
.collect();
if !axes.is_empty() {
parts.push(format!(
"this place's box is off the kit grid on {axes} and `DW0825` has already refused \
it, so the frame above is not one this map keeps",
axes = axes.join(" and "),
));
}
}
let graph_edges: BTreeMap<&str, &Edge> = c
.layout_graph
.as_ref()
.map(|g| {
g.content
.edges
.iter()
.map(|e| (e.id().0.as_str(), e))
.collect()
})
.unwrap_or_default();
let unresolved: Vec<String> = plan
.seams
.iter()
.filter(|s| {
graph_edges
.get(s.edge.0.as_str())
.is_some_and(|e| e.a() == node || e.b() == node)
&& !resolved.iter().any(|r| r.edge == s.edge)
})
.map(|s| format!("`{}`", s.edge))
.collect();
if !unresolved.is_empty() {
parts.push(format!(
"the plan writes {n} seam(s) on this place that it does not resolve ({list}), which \
`DW0828` or `DW0829` has already refused, so the allocation this piece is answering \
is short of what the plan says",
n = unresolved.len(),
list = unresolved.join(", "),
));
}
if parts.is_empty() {
return String::new();
}
format!(
" This measurement stands downstream of a site-plan refusal: {parts}. Repair the plan \
first — this line moves with it.",
parts = parts.join("; "),
)
}
fn region(plan: &SitePlanContent, placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
let r = &plan.region;
let spans = [region_span(r, 0), region_span(r, 1), region_span(r, 2)];
let region_text = format!(
"x {}..{}, y {}..{}, z {}..{}",
spans[0].0, spans[0].1, spans[1].0, spans[1].1, spans[2].0, spans[2].1
);
let mut boxes_out: Vec<Overrun<'_>> = Vec::new();
for p in placed {
let mut bad: Vec<(&'static str, i64, i64)> = Vec::new();
if !within((p.x0(), p.x1()), spans[0]) {
bad.push(("x", p.x0(), p.x1()));
}
if let Some(y) = p.y_span()
&& !within(y, spans[1])
{
bad.push(("y", y.0, y.1));
}
if !within((p.z0(), p.z1()), spans[2]) {
bad.push(("z", p.z0(), p.z1()));
}
if !bad.is_empty() {
boxes_out.push(Overrun {
index: p.index,
name: p.plan.node.0.as_str(),
axes: bad,
by: p.by.describe(),
});
}
}
if boxes_out.len() == 1 {
let o = &boxes_out[0];
d.push(Diagnostic::error(
DW_BOX_LEAVES_REGION,
"site-plan",
format!("/content/boxes/{}", o.index),
format!(
"box for `{node}` leaves the region: {bad}. It stands where it stands because it \
is {by}. The region is the whole map's extent, and it comes from the brief — a \
box is never grounds to grow it. Move the box (its pin, or the offsets of the \
seam that placed it), shrink it, or change the brief's fact and re-derive the \
region so the change is visible in the document that owns it.",
by = o.by,
node = o.name,
bad = against_region(&o.axes, &spans),
),
));
} else if boxes_out.len() > 1 {
d.push(Diagnostic::error(
DW_BOX_LEAVES_REGION,
"site-plan",
"/content/boxes",
format!(
"{n} of the {total} box(es) this plan places leave the region, which is \
{region_text}: {list}. The region is the whole map's extent, and it comes from \
the brief — a box is never grounds to grow it. One region is the cause of all \
{n} of these, which is why they are one line and not {n}: move or shrink the \
boxes, or change the brief's fact and re-derive the region so the change is \
visible in the document that owns it.",
n = boxes_out.len(),
total = placed.len(),
list = named_overruns(&boxes_out),
),
));
}
let mut volumes_out: Vec<Overrun<'_>> = Vec::new();
for (i, v) in plan.volumes.iter().enumerate() {
let vmax = v.region.max();
let mut bad: Vec<(&'static str, i64, i64)> = Vec::new();
for (axis, name) in [(0usize, "x"), (1, "y"), (2, "z")] {
if !within((v.region.min[axis], vmax[axis]), spans[axis]) {
bad.push((name, v.region.min[axis], vmax[axis]));
}
}
if !bad.is_empty() {
volumes_out.push(Overrun {
by: String::new(),
index: i,
name: v.id.0.as_str(),
axes: bad,
});
}
}
if volumes_out.len() == 1 {
let o = &volumes_out[0];
d.push(Diagnostic::error(
DW_BOX_LEAVES_REGION,
"site-plan",
format!("/content/volumes/{}", o.index),
format!(
"whole-owned volume `{id}` leaves the region: {bad}. The region is the whole's \
own extent; mass outside it is the whole growing to fit what was put in it, \
which is the direction this stage exists to forbid.",
id = o.name,
bad = against_region(&o.axes, &spans),
),
));
} else if volumes_out.len() > 1 {
d.push(Diagnostic::error(
DW_BOX_LEAVES_REGION,
"site-plan",
"/content/volumes",
format!(
"{n} of the {total} whole-owned volume(s) leave the region, which is \
{region_text}: {list}. The region is the whole's own extent; mass outside it is \
the whole growing to fit what was put in it, which is the direction this stage \
exists to forbid. One region is the cause of all {n} of these, which is why \
they are one line and not {n}.",
n = volumes_out.len(),
total = plan.volumes.len(),
list = named_overruns(&volumes_out),
),
));
}
}
fn against_region(bad: &[(&'static str, i64, i64)], spans: &[(i64, i64); 3]) -> String {
bad.iter()
.map(|(name, lo, hi)| {
let axis = match *name {
"x" => 0,
"y" => 1,
_ => 2,
};
format!(
"{name} {lo}..{hi} against the region's {}..{}",
spans[axis].0, spans[axis].1
)
})
.collect::<Vec<_>>()
.join("; ")
}
struct Overrun<'a> {
index: usize,
name: &'a str,
axes: Vec<(&'static str, i64, i64)>,
by: String,
}
fn named_overruns(items: &[Overrun<'_>]) -> String {
items
.iter()
.map(|o| {
format!(
"`{}` ({})",
o.name,
o.axes
.iter()
.map(|(ax, lo, hi)| format!("{ax} {lo}..{hi}"))
.collect::<Vec<_>>()
.join(", ")
)
})
.collect::<Vec<_>>()
.join(", ")
}
fn disjoint(placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
for (a_i, a) in placed.iter().enumerate() {
for b in &placed[a_i + 1..] {
let (Some(x), Some(z)) = (
overlap((a.x0(), a.x1()), (b.x0(), b.x1())),
overlap((a.z0(), a.z1()), (b.z0(), b.z1())),
) else {
continue;
};
let y = match (a.y_span(), b.y_span()) {
(Some(ya), Some(yb)) => match overlap(ya, yb) {
Some(y) => y,
None => continue,
},
_ => (a.floor.min(b.floor), a.floor.max(b.floor)),
};
d.push(Diagnostic::error(
DW_BOXES_OVERLAP,
"site-plan",
format!("/content/boxes/{}", b.index),
format!(
"the boxes for `{a_n}` and `{b_n}` overlap, sharing x {x0}..{x1}, y \
{y0}..{y1}, z {z0}..{z1}. Two places may share a FACE — that is what a seam \
is cut through — but never a cell: overlapping boxes are two owners for one \
block, and the derivation would have to pick between them with no rule to \
pick by. Boxes that connect sit one cell apart, and the cell between them \
is the wall they have in common.",
a_n = a.plan.node,
b_n = b.plan.node,
x0 = x.0,
x1 = x.1,
y0 = y.0,
y1 = y.1,
z0 = z.0,
z1 = z.1,
),
));
}
}
}
fn volumes_outside_boxes(plan: &SitePlanContent, placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
for (i, v) in plan.volumes.iter().enumerate() {
let vmax = v.region.max();
for p in placed {
let (Some(x), Some(z)) = (
overlap((v.region.min[0], vmax[0]), (p.x0(), p.x1())),
overlap((v.region.min[2], vmax[2]), (p.z0(), p.z1())),
) else {
continue;
};
let Some(py) = p.y_span() else { continue };
let Some(y) = overlap((v.region.min[1], vmax[1]), py) else {
continue;
};
d.push(Diagnostic::error(
DW_VOLUME_IN_BOX,
"site-plan",
format!("/content/volumes/{i}"),
format!(
"whole-owned volume `{id}` ({role}) enters the box for `{node}`, sharing x \
{x0}..{x1}, y {y0}..{y1}, z {z0}..{z1}. The whole's mass may stand beside a \
place, under it and over it; inside it, the volume and the place are two \
authorities writing one cell, and the derivation must never be asked to \
arbitrate that. Pull the volume back to the place's face, or move the \
place.",
id = v.id,
role = v.role.as_str(),
node = p.plan.node,
x0 = x.0,
x1 = x.1,
y0 = y.0,
y1 = y.1,
z0 = z.0,
z1 = z.1,
),
));
}
}
}
fn size_classes(placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
for p in placed {
let Some(class) = p.class else {
continue; };
let (kind, mut bad) = match class {
PlaceClass::Size(sc) => {
let mut bad: Vec<String> = Vec::new();
for (axis, name) in [(0usize, "x"), (1, "z")] {
let e = p.plan.extent[axis].get();
if e < sc.min_footprint[axis] || e > sc.max_footprint[axis] {
bad.push(format!(
"{e} blocks on {name}, outside the class's {}..{}",
sc.min_footprint[axis], sc.max_footprint[axis]
));
}
}
("size", bad)
}
PlaceClass::Way(w) => {
let mut bad: Vec<String> = Vec::new();
let (dx, dz) = (p.plan.extent[0].get(), p.plan.extent[1].get());
let (width, run) = (dx.min(dz), dx.max(dz));
let axis = if dx <= dz { "x" } else { "z" };
if width < w.min_width || width > w.max_width {
bad.push(format!(
"a cross-section of {width} blocks (its shorter extent, on {axis}), \
outside the class's {}..{}",
w.min_width, w.max_width
));
}
if run <= w.max_width {
bad.push(format!(
"a run of {run} blocks, which does not exceed the class's widest \
cross-section of {}. A way is a place that is longer than it is wide \
by kind and not by margin, so this box is a room — give it a \
`size_class` instead, or make it longer",
w.max_width
));
}
("way", bad)
}
};
if let Ceiling::Clearance(c) = p.plan.ceiling
&& c.get() < class.min_clearance()
{
bad.push(format!(
"{c} cells of headroom, under the class's minimum of {}",
class.min_clearance()
));
}
if bad.is_empty() {
continue;
}
d.push(Diagnostic::error(
DW_SIZE_CLASS,
"site-plan",
format!("/content/boxes/{}", p.index),
format!(
"the box for `{node}` is not built to its declared {kind} class: {bad}. The \
class is the vocabulary the graph chose this place's scale in, and this is the \
one place it becomes geometry — either build the box to it, or declare the \
place a different class in the layout graph and say so there.",
node = p.plan.node,
bad = bad.join("; "),
),
));
}
}
#[derive(Debug, Clone, Copy)]
struct SharedFace {
plane: i64,
u: (i64, i64),
v: (i64, i64),
u_axis: &'static str,
v_axis: &'static str,
}
#[derive(Debug, Clone)]
enum NotShared {
NotAdjacent { gap: i64 },
NoCommonArea { axis: &'static str },
NoPlane { which: &'static str },
}
#[derive(Clone, Copy)]
struct FaceSide {
foot: [i64; 4],
y: Option<(i64, i64)>,
}
impl Placed<'_> {
fn side(&self) -> FaceSide {
FaceSide {
foot: self.foot,
y: self.y_span(),
}
}
}
impl PlacedBox {
fn side(&self) -> FaceSide {
let (lo, hi) = self.space();
FaceSide {
foot: self.foot,
y: Some((lo[1], hi[1])),
}
}
}
fn shared_face_of(a: &PlacedBox, b: &PlacedBox, face: Face) -> Result<SharedFace, NotShared> {
shared_face(a.side(), b.side(), face)
}
fn shared_face(a: FaceSide, b: FaceSide, face: Face) -> Result<SharedFace, NotShared> {
let horizontal_pair = |plane: i64, u: (i64, i64), v: (i64, i64)| -> SharedFace {
SharedFace {
plane,
u,
v,
u_axis: "x",
v_axis: "z",
}
};
match face {
Face::East | Face::West | Face::South | Face::North => {
let (normal, along) = match face {
Face::East | Face::West => (0usize, 2usize),
_ => (2usize, 0usize),
};
let a_span = span(a.foot, normal);
let b_span = span(b.foot, normal);
let positive = matches!(face, Face::East | Face::South);
let (plane, gap) = if positive {
(a_span.1 + 1, b_span.0 - a_span.1 - 1)
} else {
(a_span.0 - 1, a_span.0 - b_span.1 - 1)
};
if gap != SHARED_FACE_GAP_CELLS {
return Err(NotShared::NotAdjacent { gap });
}
let a_along = span(a.foot, along);
let b_along = span(b.foot, along);
let u = overlap(a_along, b_along).ok_or(NotShared::NoCommonArea {
axis: if along == 0 { "x" } else { "z" },
})?;
let (ya, yb) = match (a.y, b.y) {
(Some(ya), Some(yb)) => (ya, yb),
(None, _) => return Err(NotShared::NoPlane { which: "a" }),
(_, None) => return Err(NotShared::NoPlane { which: "b" }),
};
let v = overlap(ya, yb).ok_or(NotShared::NoCommonArea { axis: "y" })?;
Ok(SharedFace {
plane,
u,
v,
u_axis: if along == 0 { "x" } else { "z" },
v_axis: "y",
})
}
Face::Up | Face::Down => {
let (Some(ya), Some(yb)) = (a.y, b.y) else {
return Err(NotShared::NoPlane {
which: if a.y.is_none() { "a" } else { "b" },
});
};
let (plane, gap) = if face == Face::Up {
(ya.1 + 1, yb.0 - ya.1 - 1)
} else {
(ya.0 - 1, ya.0 - yb.1 - 1)
};
if gap != SHARED_FACE_GAP_CELLS {
return Err(NotShared::NotAdjacent { gap });
}
let u = overlap(span(a.foot, 0), span(b.foot, 0))
.ok_or(NotShared::NoCommonArea { axis: "x" })?;
let v = overlap(span(a.foot, 2), span(b.foot, 2))
.ok_or(NotShared::NoCommonArea { axis: "z" })?;
Ok(horizontal_pair(plane, u, v))
}
}
}
struct SeamCtx<'a> {
index: usize,
seam: &'a Seam,
edge: &'a Edge,
a: &'a Placed<'a>,
b: &'a Placed<'a>,
face: SharedFace,
at: [i64; 2],
}
fn seams(
plan: &SitePlanContent,
graph: &LayoutGraphContent,
placed: &[Placed<'_>],
packed: &Packed,
table: &Metrics,
reads: &mut Reads,
d: &mut Vec<Diagnostic>,
) {
let by_node: BTreeMap<&str, &Placed<'_>> =
placed.iter().map(|p| (p.plan.node.0.as_str(), p)).collect();
let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
for (i, s) in plan.seams.iter().enumerate() {
if packed.refused.contains(&i) {
continue; }
let Some(edge) = edges.get(s.edge.0.as_str()) else {
continue; };
if matches!(edge, Edge::Vision { .. }) {
continue; }
let (Some(a), Some(b)) = (
by_node.get(edge.a().0.as_str()).copied(),
by_node.get(edge.b().0.as_str()).copied(),
) else {
continue; };
let Some(at) = packed.seam_at[i] else {
continue; };
let face = match shared_face(a.side(), b.side(), s.face) {
Ok(f) => f,
Err(why) => {
d.push(not_shared(i, s, edge, a, b, &why));
continue;
}
};
let ctx = SeamCtx {
index: i,
seam: s,
edge,
a,
b,
face,
at,
};
if !contact_declaration(&ctx, table, reads, d) {
continue;
}
let opening = if s.contact.is_some() {
None
} else {
match table.resolve(
MetricKind::Opening,
s.opening.as_deref().unwrap_or_default(),
) {
Ok(e) => match e.value(reads) {
MetricValue::Opening(o) => Some(*o),
_ => continue,
},
Err(unknown) => {
d.push(unknown.diagnostic("site-plan", &format!("/content/seams/{i}/opening")));
continue;
}
}
};
if let Some(opening) = opening {
opening_fits(&ctx, opening, d);
}
match edge {
Edge::Stair { .. } => stair(&ctx, table, reads, d),
Edge::Drop { falls, .. } => drop_seam(&ctx, *falls, table, reads, d),
Edge::Walk { .. } | Edge::Barred { .. } => {
if let Some(opening) = opening {
sill(&ctx, opening, d);
}
}
Edge::Vision { .. } => {}
}
if matches!(edge, Edge::Stair { .. }) && s.stair_in.is_none() {
d.push(Diagnostic::error(
DW_STAIR_PITCH,
"site-plan",
format!("/content/seams/{i}"),
format!(
"the seam for stair `{id}` does not say which place hosts its treads. A \
stair is massing, and massing stands somewhere: name `{a}` or `{b}` in \
`stair_in`, so that the run it costs comes out of a footprint the plan has \
already allocated rather than out of whatever space happens to be left.",
id = s.edge,
a = edge.a(),
b = edge.b(),
),
));
}
}
}
fn not_shared(
i: usize,
s: &Seam,
edge: &Edge,
a: &Placed<'_>,
b: &Placed<'_>,
why: &NotShared,
) -> Diagnostic {
let detail = match why {
NotShared::NotAdjacent { gap } if *gap < 0 => format!(
"they overlap by {} cell(s) across it rather than standing one apart",
-gap
),
NotShared::NotAdjacent { gap } => format!(
"there are {gap} cells between them across that face where a shared wall is exactly \
{SHARED_FACE_GAP_CELLS}"
),
NotShared::NoCommonArea { axis } => format!(
"they are neighbours across it, but their spans on {axis} miss each other entirely, \
so the face they share has no area to cut an opening in"
),
NotShared::NoPlane { which } => format!(
"the `{which}` end is sky-open with no stated headroom, so it has no ceiling or floor \
plane for a horizontal seam to sit in"
),
};
Diagnostic::error(
DW_SEAM_NOT_SHARED,
"site-plan",
format!("/content/seams/{i}/face"),
format!(
"the seam for `{id}` is declared on the {face} face of `{an}`, and `{an}` and `{bn}` \
do not share it: {detail}. **A seam is allocated on a face both boxes already \
have** — that is the whole of why the plan places it while both are still free to \
move, instead of two finished places discovering later that they cannot mate. \
`{an}` is x {ax0}..{ax1}, z {az0}..{az1} at floor {af}; `{bn}` is x {bx0}..{bx1}, \
z {bz0}..{bz1} at floor {bf}. Move one box against the other, or put the seam on \
the face they really share.",
id = s.edge,
face = s.face.as_str(),
an = edge.a(),
bn = edge.b(),
ax0 = a.x0(),
ax1 = a.x1(),
az0 = a.z0(),
az1 = a.z1(),
af = a.floor,
bx0 = b.x0(),
bx1 = b.x1(),
bz0 = b.z0(),
bz1 = b.z1(),
bf = b.floor,
),
)
}
fn contact_declaration(
ctx: &SeamCtx<'_>,
table: &Metrics,
reads: &mut Reads,
d: &mut Vec<Diagnostic>,
) -> bool {
let (i, s) = (ctx.index, ctx.seam);
let mut refuse = |what: String, remedy: String| {
d.push(Diagnostic::error(
DW_CONTACT,
"site-plan",
format!("/content/seams/{i}"),
format!(
"the seam for `{edge}` {what}. To fix it, {remedy}.",
edge = s.edge,
),
));
};
match (s.opening.as_ref(), s.contact.as_ref()) {
(Some(o), Some(_)) => {
refuse(
format!(
"declares BOTH an `opening` (`{o}`) and a `contact` — a hand-off is one \
kind or the other"
),
"delete whichever this is not. A portal allocates the cells a body crosses \
at and every one of them must be passable; a contact is a front along which \
two places simply meet and needs only one crossable column. The derivation \
builds them differently and the byte observer measures them differently, so \
there is no world in which a seam is both"
.to_string(),
);
return false;
}
(None, None) => {
refuse(
"declares neither an `opening` nor a `contact`, so it states no way across"
.to_string(),
format!(
"give it one. A doorway is `\"opening\": \"<name>\"` — defined \
standards: {names}. A front where the two places simply meet is \
`\"contact\": {{}}`, which spans from `at` to the far edge of the \
shared face",
names = table.names_of(MetricKind::Opening).join(", "),
),
);
return false;
}
(Some(_), None) => return true,
(None, Some(_)) => {}
}
if !matches!(ctx.edge, Edge::Walk { .. } | Edge::Drop { .. }) {
refuse(
format!(
"is a contact on a `{class}` connection, and a contact carries `walk` or \
`drop` only",
class = ctx.edge.class(),
),
"give the seam a standard `opening`, or declare the connection `walk` or \
`drop` in the layout graph. A stair needs a run and a pitch, a barred door \
needs a gate region that seals and clears, and a sightline is not a crossing \
at all — none of the three is a thing a front can be, and this engine does \
not have them as contacts until a campaign brief demands one"
.to_string(),
);
return false;
}
let Some(floor) = table.broadest_opening_width(reads) else {
return false; };
let (u_span, v_span) = (ctx.face.u, ctx.face.v);
let (u_hi, v_hi) = crossing_hi(ctx);
let width = u_hi - ctx.at[0] + 1;
if width <= i64::from(floor) {
refuse(
format!(
"is a contact {width} cell(s) wide, which is not wider than the broadest \
standard opening ({floor} cells)"
),
format!(
"widen the span, or declare it a portal — anything this narrow could have \
been one, and a doorway called a contact would dodge the standard set \
while every downstream door check went on being wrong about it. Defined \
openings: {names}",
names = table.names_of(MetricKind::Opening).join(", "),
),
);
return false;
}
let mut off: Vec<String> = Vec::new();
if ctx.at[0] < u_span.0 || u_hi > u_span.1 {
off.push(format!(
"{}..{} on {}, against the face's {}..{}",
ctx.at[0], u_hi, ctx.face.u_axis, u_span.0, u_span.1
));
}
if ctx.at[1] < v_span.0 || v_hi > v_span.1 {
off.push(format!(
"{}..{} on {}, against the face's {}..{}",
ctx.at[1], v_hi, ctx.face.v_axis, v_span.0, v_span.1
));
}
if !off.is_empty() {
refuse(
format!(
"is a contact whose span leaves the face the two boxes share: {}",
off.join("; ")
),
"move `at` onto the shared face, or shorten `contact.extent` — the span is \
where the derivation writes no wall, and a span running off the face would \
ask it to open a wall that is not there. Omitting `contact.extent` runs the \
span from `at` to the far edge of the face, which never leaves it"
.to_string(),
);
return false;
}
true
}
fn crossing_hi(ctx: &SeamCtx<'_>) -> (i64, i64) {
let e = contact_extent(ctx.seam, ctx.at, &ctx.face);
(ctx.at[0] + e[0] - 1, ctx.at[1] + e[1] - 1)
}
fn opening_fits(ctx: &SeamCtx<'_>, opening: crate::metrics::Opening, d: &mut Vec<Diagnostic>) {
let (i, s, edge, face, at) = (ctx.index, ctx.seam, ctx.edge, &ctx.face, ctx.at);
let anchor_in =
at[0] >= face.u.0 && at[0] <= face.u.1 && at[1] >= face.v.0 && at[1] <= face.v.1;
if !anchor_in {
d.push(Diagnostic::error(
DW_SEAM_NOT_SHARED,
"site-plan",
format!("/content/seams/{i}/at"),
format!(
"the seam for `{id}` is anchored at {ua} {u}, {va} {v}, which is not on the face \
`{an}` and `{bn}` share — that face runs {ua} {u0}..{u1} by {va} {v0}..{v1} in \
the plane at {plane}. `at` names the opening's low corner in the face's own two \
axes, so a corner off the face allocates the seam nowhere.",
id = s.edge,
an = edge.a(),
bn = edge.b(),
ua = face.u_axis,
va = face.v_axis,
u = at[0],
v = at[1],
u0 = face.u.0,
u1 = face.u.1,
v0 = face.v.0,
v1 = face.v.1,
plane = face.plane,
),
));
return;
}
let u_hi = at[0] + i64::from(opening.width) - 1;
let v_hi = at[1] + i64::from(opening.height) - 1;
if u_hi <= face.u.1 && v_hi <= face.v.1 {
return;
}
d.push(Diagnostic::error(
DW_SEAM_OPENING,
"site-plan",
format!("/content/seams/{i}/opening"),
format!(
"the `{name}` opening ({w}x{h}) does not fit on the face `{an}` and `{bn}` share. \
Anchored at {ua} {u}, {va} {v} it would run to {ua} {u_hi}, {va} {v_hi}, and the \
shared face ends at {ua} {u1}, {va} {v1}. Move the anchor, choose a narrower \
standard opening, or grow the overlap between the two boxes — the standard set is \
the vocabulary, so the opening is never quietly cropped to fit.",
name = s.opening.as_deref().unwrap_or_default(),
w = opening.width,
h = opening.height,
an = edge.a(),
bn = edge.b(),
ua = face.u_axis,
va = face.v_axis,
u = at[0],
v = at[1],
u1 = face.u.1,
v1 = face.v.1,
),
));
}
fn sill(ctx: &SeamCtx<'_>, opening: crate::metrics::Opening, d: &mut Vec<Diagnostic>) {
let (i, s, edge, a, b, face) = (ctx.index, ctx.seam, ctx.edge, ctx.a, ctx.b, &ctx.face);
if face.v_axis != "y" {
return; }
let sources: Vec<(&NodeId, &Placed<'_>)> = match edge.direction() {
Some(crate::layout::Direction::AToB) => vec![(edge.a(), a)],
Some(crate::layout::Direction::BToA) => vec![(edge.b(), b)],
None => vec![(edge.a(), a), (edge.b(), b)],
};
let max_rise = MAX_JUMP_RISE_16 / crate::metrics::FULL_16;
for (name, p) in sources {
let rise = ctx.at[1] - p.floor;
if rise <= max_rise {
continue;
}
d.push(Diagnostic::error(
DW_SEAM_OPENING,
"site-plan",
format!("/content/seams/{i}/at"),
format!(
"the seam for `{id}` has its sill at y {sill}, {rise} blocks over the floor of \
`{name}` at y {floor}, and a body reaches at most {max_rise} block(s) by \
jumping ({j}/16 of vanilla's apex). A body entering from `{name}` cannot get \
into the opening at all, so the connection the graph declares is not one. The \
sill is the higher of the two floors: bring the floors within a step of each \
other, or declare the connection a `stair` and let the treads carry the climb. \
(The opening is {w}x{h}.)",
id = s.edge,
sill = ctx.at[1],
j = MAX_JUMP_RISE_16,
floor = p.floor,
w = opening.width,
h = opening.height,
),
));
}
}
fn stair(ctx: &SeamCtx<'_>, table: &Metrics, reads: &mut Reads, d: &mut Vec<Diagnostic>) {
let (i, s, edge, a, b, face) = (ctx.index, ctx.seam, ctx.edge, ctx.a, ctx.b, &ctx.face);
let rise = b.floor - a.floor;
if rise == 0 {
d.push(Diagnostic::error(
DW_STAIR_PITCH,
"site-plan",
format!("/content/seams/{i}"),
format!(
"`{id}` is a stair, and `{an}` and `{bn}` are both on plane y {f} — so it climbs \
nothing. A stair's rise is not authored here: it is the difference between the \
two floors the plan has already chosen, which means a stair between two places \
at one level is a walk that has been called a stair. Move one floor, or declare \
the connection a `walk`.",
id = s.edge,
an = edge.a(),
bn = edge.b(),
f = a.floor,
),
));
return;
}
let Some(host_id) = &s.stair_in else {
return; };
let (low, high) = if b.floor > a.floor {
(edge.a(), edge.b())
} else {
(edge.b(), edge.a())
};
if host_id == high {
d.push(Diagnostic::error(
DW_STAIR_PITCH,
"site-plan",
format!("/content/seams/{i}/stair_in"),
format!(
"the stair for `{id}` hosts its treads in `{high}`, which is the HIGHER of the two \
places (`{an}` stands at y {af}, `{bn}` at y {bf}). Treads rise off a walk plane, \
and the only plane this stair can rise off is the lower one — massing in the \
upper place would have to start at that place's floor and reach a level beneath \
it, which is not a stair. Host it in `{low}`, and check that `{low}` affords the \
run: a stair costs its footprint, and moving the host moves who pays.",
id = s.edge,
an = edge.a(),
bn = edge.b(),
af = a.floor,
bf = b.floor,
),
));
return;
}
let host = if host_id == edge.a() { a } else { b };
let Some((_, extent)) = crossing_rect(s, ctx.at, face, table, reads) else {
return; };
let Some(run) = stair_run(
host.floor,
host.foot,
normal_axis_of(s.face),
face.plane,
crossing_aabb(s, ctx.at, face, extent),
) else {
return; };
let run_axis = if run.run_axis == 0 { "x" } else { "z" };
if gentlest_pitch(table, reads, run.climb, run.available).is_some() {
return; }
let Some((name, needed)) = tightest_pitch(table, reads, run.climb) else {
return; };
let carries = if run.climb == rise.abs() {
String::new()
} else {
format!(
" The treads carry {climb}, not {rise}: they rise off `{host_id}`'s floor at \
{hf} and stop at {target}, which is where the opening puts a body.",
climb = run.climb,
rise = rise.abs(),
hf = host.floor,
target = host.floor + run.climb,
)
};
d.push(Diagnostic::error(
DW_STAIR_PITCH,
"site-plan",
format!("/content/seams/{i}"),
format!(
"the stair for `{id}` climbs {rise} block(s) between `{an}` (floor {af}) and `{bn}` \
(floor {bf}), and no standard pitch fits inside `{host_id}`. The tightest standard \
is `{name}`, which needs {needed} block(s) of run for a climb of {climb}, and \
`{host_id}` affords {available} of run on {run_axis}.{carries} Give the host a \
longer footprint on that axis, move the opening so the run has more room beside it, \
host the stair in the other place, or bring the two floors closer together — the \
pitches are standards, so a steeper one is not on offer.",
id = s.edge,
an = edge.a(),
bn = edge.b(),
af = a.floor,
bf = b.floor,
rise = rise.abs(),
climb = run.climb,
available = run.available,
),
));
}
fn drop_seam(
ctx: &SeamCtx<'_>,
falls: crate::layout::Direction,
table: &Metrics,
reads: &mut Reads,
d: &mut Vec<Diagnostic>,
) {
let (i, s, edge, a, b) = (ctx.index, ctx.seam, ctx.edge, ctx.a, ctx.b);
let (from, from_p, to, to_p) = match falls {
crate::layout::Direction::AToB => (edge.a(), a, edge.b(), b),
crate::layout::Direction::BToA => (edge.b(), b, edge.a(), a),
};
let depth = from_p.floor - to_p.floor;
if depth <= 0 {
d.push(Diagnostic::error(
DW_DROP_POLICY,
"site-plan",
format!("/content/seams/{i}"),
format!(
"`{id}` falls from `{from}` (floor {ff}) into `{to}` (floor {tf}), which is \
{what}. A drop is one-way because a body cannot climb back up the way it came, \
and that is only true going down — this one is a mislabelled stair. Swap the \
declared direction, move the floors, or declare the connection a `stair`.",
id = s.edge,
ff = from_p.floor,
tf = to_p.floor,
what = if depth == 0 {
"the same plane".to_string()
} else {
format!("{} block(s) HIGHER", -depth)
},
),
));
return;
}
let Some(cap) = table.max_designed_drop_blocks(reads) else {
return;
};
if depth <= i64::from(cap) {
return;
}
d.push(Diagnostic::error(
DW_DROP_POLICY,
"site-plan",
format!("/content/seams/{i}"),
format!(
"`{id}` drops {depth} blocks from `{from}` into `{to}`, and the designed-drop policy \
caps a declared fall at {cap}. This is a **policy** cap and it is deliberately far \
tighter than what a body survives: a drop is a decision about the shape of the map, \
and it should not also be a decision about the party's health. Bring the two floors \
closer, or break the fall with a place between them.",
id = s.edge,
),
));
}
enum Measured {
Value(f64),
Unresolved(Diagnostic),
}
fn identities(
c: &Campaign,
plan: &SitePlanContent,
placed: &[Placed<'_>],
d: &mut Vec<Diagnostic>,
) {
let facts: BTreeMap<&str, &crate::layout::BriefFact> = c
.geometry_brief
.as_ref()
.map(|b| {
b.content
.facts
.iter()
.map(|f| (f.id.0.as_str(), f))
.collect()
})
.unwrap_or_default();
if facts.is_empty() || plan.identities.is_empty() {
let empty = match (facts.is_empty(), plan.identities.is_empty()) {
(true, true) => "the brief states no fact and the plan declares no identity",
(true, false) => "the brief states no fact",
_ => "the plan declares no identity",
};
d.push(Diagnostic::warning(
DW_IDENTITY_EMPTY,
"site-plan",
"/content/identities",
format!(
"the identity gate binds nothing: {empty}. This is what holds the whole map to \
the design somebody wrote down — with either side empty, the plan may say \
anything at all and every check above will still pass, because none of them \
has an opinion about how big the map was meant to be. It is a warning rather \
than a refusal so that a deliberately minimal plan stays compilable; it is \
printed every run so that the emptiness is never quietly a pass."
),
));
}
let by_node: BTreeMap<&str, &Placed<'_>> =
placed.iter().map(|p| (p.plan.node.0.as_str(), p)).collect();
let datums: BTreeMap<&str, i64> = plan.datums.iter().map(|x| (x.id.0.as_str(), x.y)).collect();
for (i, id) in plan.identities.iter().enumerate() {
let Some(fact) = facts.get(id.fact.0.as_str()) else {
d.push(Diagnostic::error(
DW_PLAN_AGREEMENT,
"site-plan",
format!("/content/identities/{i}/fact"),
format!(
"this identity holds the map to `{f}`, which the geometry brief states no \
fact for. An identity binds to a number the brief WROTE DOWN — that is what \
makes it a design being kept rather than an assertion the plan makes about \
itself.",
f = id.fact,
),
));
continue;
};
let measured = measure(&id.measure, plan, &by_node, &datums, i);
let value = match measured {
Measured::Value(v) => v,
Measured::Unresolved(diag) => {
d.push(diag);
continue;
}
};
if id.cmp.holds(value, fact.value) {
continue;
}
d.push(Diagnostic::error(
DW_IDENTITY_FALSE,
"site-plan",
format!("/content/identities/{i}"),
format!(
"the plan does not keep `{f}`: {what} measures {value}, and the brief asks for \
{cmp} {want}{unit}. The brief's sentence was: \"{note}\". Either move the \
geometry until the number is true, or change the brief's fact — in the brief, \
where the design is written down, so that the change is a decision somebody \
took rather than a plan that drifted.",
f = id.fact,
what = describe(&id.measure),
cmp = id.cmp.as_str(),
want = fact.value,
unit = fact
.unit
.as_ref()
.map(|u| format!(" {u}"))
.unwrap_or_default(),
note = fact.note,
),
));
}
}
fn measure(
m: &Measure,
plan: &SitePlanContent,
by_node: &BTreeMap<&str, &Placed<'_>>,
datums: &BTreeMap<&str, i64>,
i: usize,
) -> Measured {
let missing_node = |node: &NodeId| {
Measured::Unresolved(Diagnostic::error(
DW_PLAN_AGREEMENT,
"site-plan",
format!("/content/identities/{i}/measure"),
format!(
"this identity measures `{node}`, which this plan embeds no box for. A measure \
is taken off the geometry, so it can only name a place the plan actually put \
somewhere."
),
))
};
match m {
Measure::RegionExtent { axis } => {
Measured::Value(f64::from(plan.region.extent[axis.index()].get()))
}
Measure::BoxExtent { node, axis } => match by_node.get(node.0.as_str()) {
Some(p) => Measured::Value(f64::from(p.plan.extent[axis.index()].get())),
None => missing_node(node),
},
Measure::BoxHeight { node } => match by_node.get(node.0.as_str()) {
Some(p) => match p.clearance {
Some(c) => Measured::Value(f64::from(c)),
None => Measured::Unresolved(Diagnostic::error(
DW_PLAN_AGREEMENT,
"site-plan",
format!("/content/identities/{i}/measure"),
format!(
"this identity measures the height of `{node}`, which is sky-open and \
whose size class did not resolve — so the plan states no headroom for \
it at all. Fix the class name the layout graph declares (`DW0812` names \
it) and the height becomes the class's own minimum."
),
)),
},
None => missing_node(node),
},
Measure::DistanceXz { from, to } => {
let (Some(a), Some(b)) = (by_node.get(from.0.as_str()), by_node.get(to.0.as_str()))
else {
return missing_node(if by_node.contains_key(from.0.as_str()) {
to
} else {
from
});
};
let (ax, az) = a.centre_xz();
let (bx, bz) = b.centre_xz();
Measured::Value(((bx - ax).powi(2) + (bz - az).powi(2)).sqrt())
}
Measure::DatumY { datum } => match datums.get(datum.0.as_str()) {
Some(y) => Measured::Value(*y as f64),
None => Measured::Unresolved(Diagnostic::error(
crate::codes::DANGLING_REF,
"site-plan",
format!("/content/identities/{i}/measure"),
format!(
"this identity measures `{datum}`, which this plan declares no `datums[]` \
entry for."
),
)),
},
}
}
fn describe(m: &Measure) -> String {
match m {
Measure::RegionExtent { axis } => {
format!("the region's extent on {}", axis.as_str())
}
Measure::BoxExtent { node, axis } => {
format!("`{node}`'s footprint on {}", axis.as_str())
}
Measure::BoxHeight { node } => format!("`{node}`'s headroom"),
Measure::DistanceXz { from, to } => {
format!("the horizontal distance from `{from}` to `{to}`")
}
Measure::DatumY { datum } => format!("the plane `{datum}`"),
}
}
impl Axis {
fn index(self) -> usize {
match self {
Axis::X => 0,
Axis::Y => 1,
Axis::Z => 2,
}
}
fn as_str(self) -> &'static str {
match self {
Axis::X => "x",
Axis::Y => "y",
Axis::Z => "z",
}
}
}
impl PlanAxis {
fn index(self) -> usize {
match self {
PlanAxis::X => 0,
PlanAxis::Z => 1,
}
}
fn as_str(self) -> &'static str {
match self {
PlanAxis::X => "x",
PlanAxis::Z => "z",
}
}
}
impl VolumeRole {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
VolumeRole::Massif => "massif",
VolumeRole::Ground => "ground",
VolumeRole::Clearance => "clearance",
}
}
}
fn lighting(plan: &SitePlanContent, d: &mut Vec<Diagnostic>) {
let Some(l) = &plan.lighting else { return };
if (1..=14).contains(&l.min_light) {
return;
}
d.push(Diagnostic::error(
crate::codes::LIGHTING_RANGE,
"site-plan",
"/content/lighting/min_light",
format!(
"`lighting.min_light` = {} is out of range — set it to a value in 1..=14 (7 is the \
default)",
l.min_light
),
));
}
#[must_use]
pub fn passable_opening_cells() -> (u32, u32) {
(passable_width_cells(), passable_clearance_cells())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_run_beside_a_pierced_floor_is_the_room_on_one_side_plus_the_hole() {
let run = stair_run(56, [4, 11, 4, 11], 1, 63, ([7, 63, 6], [9, 63, 8]))
.expect("the hole is over this host and the plane is above its floor");
assert_eq!(run.climb, 7, "56 up to the pierced plane at 63");
assert_eq!(run.run_axis, 0, "the host is square, so the run takes x");
assert_eq!(
run.available, 6,
"three cells of room on the low side, plus the hole's three"
);
assert_eq!(
run.start, 9,
"course 0 stands under the far edge of the hole"
);
assert_eq!(run.step, -1, "and the run walks back into the room");
}
#[test]
fn no_standard_pitch_fits_a_climb_of_seven_in_six_cells_of_run() {
let table = Metrics::table();
let mut reads = Reads::default();
assert!(gentlest_pitch(&table, &mut reads, 7, 6).is_none());
assert!(gentlest_pitch(&table, &mut reads, 7, 7).is_some());
assert_eq!(tightest_pitch(&table, &mut reads, 7), Some(("stair", 7)));
}
#[test]
fn a_run_across_a_vertical_face_is_the_whole_footprint() {
let run = stair_run(64, [4, 11, 4, 11], 0, 12, ([12, 72, 6], [12, 74, 8]))
.expect("the wall is beyond the host, so the run walks back from it");
assert_eq!(run.climb, 8, "64 up to the sill at 72");
assert_eq!(run.run_axis, 0);
assert_eq!(run.available, 8, "the host's whole extent on x");
assert_eq!(run.start, 11, "course 0 stands against the wall");
assert_eq!(run.step, -1);
}
}