use super::*;
pub(super) 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()));
}
let y = p.y_span();
if !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.rsplit(' ').next().unwrap_or(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(", ")
}
pub(super) fn roofs(placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
for p in placed {
let Some(roof) = p.plan.roof else { continue };
if matches!(p.plan.ceiling, Ceiling::Open(_)) {
d.push(Diagnostic::error(
DW_ROOF_NO_ROOM,
"site-plan",
format!("/content/boxes/{}/roof", p.index),
format!(
"the box for `{node}` declares a roof ({c} course(s), eaves {e}) and its \
ceiling is `open`. A sky-open place has no lid to put a roof on: it claims \
its ground and its headroom and nothing above that. Remove the roof, or \
give the place a `clearance` ceiling so the whole can reserve a roof over \
it.",
node = p.plan.node,
c = roof.courses,
e = roof.eaves,
),
));
continue;
}
let (_, top) = p.y_span();
if roof.courses == 0 {
continue;
}
let (rlo, rhi) = ((top + 2), (top + 1 + i64::from(roof.courses)));
let (sx0, sx1, sz0, sz1) = (p.x0() - 1, p.x1() + 1, p.z0() - 1, p.z1() + 1);
for q in placed {
if q.index == p.index {
continue;
}
let q_top = q.y_span().1;
let in_space = overlap((sx0, sx1), (q.x0(), q.x1()))
.zip(overlap((sz0, sz1), (q.z0(), q.z1())))
.zip(overlap((rlo, rhi), (q.floor, q_top)));
let in_floor = overlap((sx0, sx1), (q.x0() - 1, q.x1() + 1))
.zip(overlap((sz0, sz1), (q.z0() - 1, q.z1() + 1)))
.zip(overlap((rlo, rhi), (q.floor - 1, q.floor - 1)));
let Some(((x, z), y)) = in_space.or(in_floor) else {
continue;
};
d.push(Diagnostic::error(
DW_ROOF_NO_ROOM,
"site-plan",
format!("/content/boxes/{}/roof", p.index),
format!(
"the roof over `{a}` rises into `{b}`: its courses y {ry0}..{ry1} meet \
`{b}`'s floor and play space at x {x0}..{x1}, y {y0}..{y1}, z {z0}..{z1}. \
A roof is the whole reserving the volume a building will take, and that \
volume cannot be a place somebody stands in. Declare fewer courses, raise \
`{b}`, or make the two one place whose piece carries both.",
a = p.plan.node,
b = q.plan.node,
ry0 = rlo,
ry1 = rhi,
x0 = x.0,
x1 = x.1,
y0 = y.0,
y1 = y.1,
z0 = z.0,
z1 = z.1,
),
));
}
}
}
pub(super) 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 Some(y) = overlap(a.y_span(), b.y_span()) else {
continue;
};
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,
),
));
}
}
}
pub(super) 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(y) = overlap((v.region.min[1], vmax[1]), p.y_span()) 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,
),
));
}
}
}