use super::*;
#[derive(Debug, Clone)]
pub(super) struct Placed<'a> {
pub(super) index: usize,
pub(super) plan: &'a PlanBox,
pub(super) foot: [i64; 4],
pub(super) floor: i64,
pub(super) clearance: Option<u32>,
pub(super) class: Option<PlaceClass>,
pub(super) by: Provenance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PlaceClass {
Size(SizeClass),
Way(WayClass),
}
impl PlaceClass {
pub(super) fn min_clearance(self) -> u32 {
match self {
PlaceClass::Size(c) => c.min_clearance,
PlaceClass::Way(w) => w.min_clearance,
}
}
}
impl Placed<'_> {
pub(super) fn x0(&self) -> i64 {
self.foot[0]
}
pub(super) fn x1(&self) -> i64 {
self.foot[1]
}
pub(super) fn z0(&self) -> i64 {
self.foot[2]
}
pub(super) fn z1(&self) -> i64 {
self.foot[3]
}
pub(super) fn y_span(&self) -> Option<(i64, i64)> {
let c = i64::from(self.clearance?);
Some((self.floor, self.floor + c - 1))
}
pub(super) 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()
}
pub(super) 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,
}
}
pub(super) 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,
}
}
pub(super) 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 !edge.has_seam() {
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),
}
}
pub(super) fn span(foot: [i64; 4], axis: usize) -> (i64, i64) {
if axis == 0 {
(foot[0], foot[1])
} else {
(foot[2], foot[3])
}
}
pub(super) 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))
}
pub(super) fn within(r: (i64, i64), w: (i64, i64)) -> bool {
r.0 >= w.0 && r.1 <= w.1
}
pub(super) fn region_span(region: &WorldBox, axis: usize) -> (i64, i64) {
(region.min[axis], region.max()[axis])
}
#[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);
}
}