Skip to main content

delvewright_dsl/siteplan/
pack.rs

1//! Packing (spec-0059 §3): a box is placed by a pin or by a seam that hangs it
2//! off a box that already stands, and the grid is derived (`DW0883`).
3
4use super::*;
5
6// ---------------------------------------------------------------------------
7// Packing — a box is placed by its seam, and the grid is derived (spec-0059 §3)
8// ---------------------------------------------------------------------------
9
10/// How a box came to stand where it stands.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Provenance {
13    /// The author pinned its `min`.
14    Pinned,
15    /// A seam hung it off a box that already stood.
16    Seam {
17        /// Index into `seams[]`.
18        seam: usize,
19        /// The connection the seam allocates.
20        edge: EdgeId,
21        /// The box it was hung off.
22        from: NodeId,
23        /// The face of `from` it hangs off — or, when `from` is the seam's `b`
24        /// end, the face of this box the seam names.
25        face: Face,
26    },
27}
28
29impl Provenance {
30    /// The words a refusal or the placing line uses.
31    pub(super) fn describe(&self) -> String {
32        match self {
33            Provenance::Pinned => "pinned".to_string(),
34            Provenance::Seam {
35                edge, from, face, ..
36            } => format!(
37                "hung off `{from}` across the {face} face by the seam for `{edge}`",
38                face = face.as_str()
39            ),
40        }
41    }
42}
43
44/// One box's corner, with its provenance.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct PackedBox {
47    /// The place.
48    pub node: NodeId,
49    /// Low corner `[x, z]`, in world coordinates.
50    pub min: [i64; 2],
51    /// Where the corner came from.
52    pub by: Provenance,
53}
54
55/// The packing of one plan: every corner the pins and seams settle, and the
56/// world anchor of every seam whose two ends stand.
57#[derive(Debug, Default)]
58pub(super) struct Packed {
59    /// Per `plan.boxes` index; `None` when nothing placed the box.
60    boxes: Vec<Option<PackedBox>>,
61    /// Per `plan.seams` index: the crossing's low corner on the face's own two
62    /// world axes — `[along, sill]` on a wall, `[x, z]` through a floor or
63    /// ceiling. `None` when either end is unplaced or a floor is unresolved.
64    pub(super) seam_at: Vec<Option<[i64; 2]>>,
65    /// Seams the packing itself refused; [`seams`] does not judge them twice.
66    pub(super) refused: BTreeSet<usize>,
67    /// Connected components of the seam graph, over boxes the graph declares.
68    pub(super) components: usize,
69    pub(super) pinned: usize,
70    pub(super) derived: usize,
71}
72
73/// A seam resolved far enough to place a box: its two box indices and its two
74/// offsets, each `[u, v]` on a horizontal face and `[u, 0]` on a vertical one.
75#[derive(Debug, Clone, Copy)]
76struct Link {
77    a: usize,
78    b: usize,
79    at: [i64; 2],
80    meets: [i64; 2],
81}
82
83/// The two horizontal-axis indices a face's arithmetic uses: `(normal, along)`
84/// into `[x, z]`.
85fn face_axes(face: Face) -> (usize, usize) {
86    match face {
87        Face::East | Face::West => (0, 1),
88        Face::North | Face::South => (1, 0),
89        Face::Up | Face::Down => (0, 1), // unused: a horizontal face has no normal in [x, z]
90    }
91}
92
93/// The width a crossing is centred by, on the face's own two axes, or `None`
94/// for a contact with no extent — the whole face, so the offsets default to 0.
95/// `Err` when the opening cannot be resolved (`DW0812`/`DW0876` name that).
96fn centring_width(s: &Seam, table: &Metrics, reads: &mut Reads) -> Result<Option<[i64; 2]>, ()> {
97    if let Some(c) = &s.contact {
98        return Ok(c
99            .extent
100            .map(|e| [i64::from(e[0].get()), i64::from(e[1].get())]));
101    }
102    let Some(spec) = s.opening.as_ref() else {
103        return Err(());
104    };
105    let o = spec.resolve(table, reads).map_err(|_| ())?;
106    Ok(Some([i64::from(o.width), i64::from(o.height)]))
107}
108
109/// Why an offset is not a position on its face.
110enum OffsetProblem {
111    /// The shape does not match the face: an integer through a floor, a pair
112    /// on a wall.
113    Shape { which: &'static str },
114    /// The crossing, anchored there, leaves the box's own face.
115    OffFace {
116        which: &'static str,
117        axis: &'static str,
118        value: i64,
119        extent: i64,
120        width: i64,
121    },
122}
123
124/// Resolve `at` and `meets` for one seam against its two boxes' extents:
125/// declared values taken as written, omitted ones centred (spec-0059 §3).
126fn offsets(
127    s: &Seam,
128    ext_a: [i64; 2],
129    ext_b: [i64; 2],
130    width: Option<[i64; 2]>,
131) -> Result<([i64; 2], [i64; 2]), OffsetProblem> {
132    let horizontal = s.face.is_horizontal_plane();
133    let one = |which: &'static str,
134               declared: Option<Offset>,
135               ext: [i64; 2]|
136     -> Result<[i64; 2], OffsetProblem> {
137        // Which of `[dx, dz]` each offset component runs along, and its name.
138        let (axes, names): ([usize; 2], [&'static str; 2]) = if horizontal {
139            ([0, 1], ["x", "z"])
140        } else {
141            let (_, along) = face_axes(s.face);
142            ([along, 0], [if along == 0 { "x" } else { "z" }, ""])
143        };
144        // Centred; a crossing wider than the face centres at the corner and
145        // whether it fits is `DW0829`'s or `DW0876`'s question.
146        let default = |i: usize| match width {
147            Some(w) => (ext[axes[i]] - w[i]).div_euclid(2).max(0),
148            None => 0,
149        };
150        let off = match (declared, horizontal) {
151            (None, true) => [default(0), default(1)],
152            (None, false) => [default(0), 0],
153            (Some(Offset::Plane(p)), true) => p,
154            (Some(Offset::Along(u)), false) => [u, 0],
155            _ => return Err(OffsetProblem::Shape { which }),
156        };
157        let n = if horizontal { 2 } else { 1 };
158        for i in 0..n {
159            // The CORNER is on the face; whether the crossing fits from there is
160            // `DW0829`'s (a portal) or `DW0876`'s (a contact) question, as ever.
161            let extent = ext[axes[i]];
162            let w = width.map_or(1, |w| w[i]);
163            if off[i] < 0 || off[i] > extent - 1 {
164                return Err(OffsetProblem::OffFace {
165                    which,
166                    axis: names[i],
167                    value: off[i],
168                    extent,
169                    width: w,
170                });
171            }
172        }
173        Ok(off)
174    };
175    Ok((one("at", s.at, ext_a)?, one("meets", s.meets, ext_b)?))
176}
177
178/// The corner of the box a seam places, from the corner of the one that
179/// already stands. `from_a` places `b` off `a`; otherwise `a` off `b`.
180fn derive_corner(
181    face: Face,
182    known: [i64; 2],
183    ext_a: [i64; 2],
184    ext_b: [i64; 2],
185    at: [i64; 2],
186    meets: [i64; 2],
187    from_a: bool,
188) -> [i64; 2] {
189    if face.is_horizontal_plane() {
190        return if from_a {
191            [known[0] + at[0] - meets[0], known[1] + at[1] - meets[1]]
192        } else {
193            [known[0] - at[0] + meets[0], known[1] - at[1] + meets[1]]
194        };
195    }
196    let (normal, along) = face_axes(face);
197    let positive = matches!(face, Face::East | Face::South);
198    let mut out = [0i64; 2];
199    if from_a {
200        out[along] = known[along] + at[0] - meets[0];
201        out[normal] = if positive {
202            known[normal] + ext_a[normal] + 1
203        } else {
204            known[normal] - ext_b[normal] - 1
205        };
206    } else {
207        out[along] = known[along] - at[0] + meets[0];
208        out[normal] = if positive {
209            known[normal] - ext_a[normal] - 1
210        } else {
211            known[normal] + ext_b[normal] + 1
212        };
213    }
214    out
215}
216
217/// The crossing's low corner in the face's own two world axes — what every
218/// seam rule judges. `[along, sill]` on a wall, the sill being the higher of
219/// the two floors; `[x, z]` through a floor or ceiling.
220fn crossing_anchor(
221    face: Face,
222    a_min: [i64; 2],
223    at: [i64; 2],
224    floor_a: i64,
225    floor_b: i64,
226) -> [i64; 2] {
227    if face.is_horizontal_plane() {
228        [a_min[0] + at[0], a_min[1] + at[1]]
229    } else {
230        let (_, along) = face_axes(face);
231        [a_min[along] + at[0], floor_a.max(floor_b)]
232    }
233}
234
235fn ext_of(b: &PlanBox) -> [i64; 2] {
236    [i64::from(b.extent[0].get()), i64::from(b.extent[1].get())]
237}
238
239/// **The packing** (spec-0059 §3): the pinned boxes seed it; then `seams[]` in
240/// document order, repeatedly, each seam with exactly one end standing placing
241/// the other, until a pass places nothing. A seam whose two ends both stand is
242/// then checked — the corner it would derive against the corner the box has —
243/// and a component of the seam graph with no pinned box is refused, because
244/// nothing places it.
245fn pack(
246    plan: &SitePlanContent,
247    graph: &LayoutGraphContent,
248    table: &Metrics,
249    floors: &[Option<i64>],
250    reads: &mut Reads,
251    d: &mut Vec<Diagnostic>,
252) -> Packed {
253    let mut out = Packed {
254        boxes: vec![None; plan.boxes.len()],
255        seam_at: vec![None; plan.seams.len()],
256        ..Packed::default()
257    };
258    let nodes: BTreeSet<&str> = graph.nodes.iter().map(|n| n.id.0.as_str()).collect();
259    let mut by_node: BTreeMap<&str, usize> = BTreeMap::new();
260    for (i, b) in plan.boxes.iter().enumerate() {
261        if nodes.contains(b.node.0.as_str()) {
262            by_node.entry(b.node.0.as_str()).or_insert(i);
263        }
264    }
265    let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
266
267    // ---- seeds
268    for (i, b) in plan.boxes.iter().enumerate() {
269        if let Some(min) = b.min {
270            out.boxes[i] = Some(PackedBox {
271                node: b.node.clone(),
272                min,
273                by: Provenance::Pinned,
274            });
275            out.pinned += 1;
276        }
277    }
278
279    // ---- the links: every seam whose edge and boxes resolve, with its offsets
280    let mut pairs: Vec<(usize, usize)> = Vec::new();
281    let mut links: Vec<Option<Link>> = Vec::with_capacity(plan.seams.len());
282    for (i, s) in plan.seams.iter().enumerate() {
283        let Some(edge) = edges.get(s.edge.0.as_str()) else {
284            links.push(None);
285            continue; // `DW0824` refused the reference.
286        };
287        if !edge.has_seam() {
288            links.push(None);
289            continue; // `DW0824` said this carries a sightline.
290        }
291        let (Some(&a), Some(&b)) = (
292            by_node.get(edge.a().0.as_str()),
293            by_node.get(edge.b().0.as_str()),
294        ) else {
295            links.push(None);
296            continue; // `DW0824` reported the missing box.
297        };
298        pairs.push((a, b));
299        // An unresolvable opening (`DW0812`/`DW0876` say so) centres nothing:
300        // the crossing is taken one cell wide at the corner, so the seam still
301        // places and the rules that name the opening run over a plan that
302        // stands.
303        let width = centring_width(s, table, reads).unwrap_or_default();
304        match offsets(s, ext_of(&plan.boxes[a]), ext_of(&plan.boxes[b]), width) {
305            Ok((at, meets)) => links.push(Some(Link { a, b, at, meets })),
306            Err(problem) => {
307                d.push(offset_problem(i, s, edge, problem));
308                out.refused.insert(i);
309                links.push(None);
310            }
311        }
312    }
313
314    // ---- placement passes
315    loop {
316        let mut changed = false;
317        for (i, link) in links.iter().enumerate() {
318            let Some(l) = link else { continue };
319            let s = &plan.seams[i];
320            let (ext_a, ext_b) = (ext_of(&plan.boxes[l.a]), ext_of(&plan.boxes[l.b]));
321            match (out.boxes[l.a].clone(), out.boxes[l.b].clone()) {
322                (Some(a), None) => {
323                    let min = derive_corner(s.face, a.min, ext_a, ext_b, l.at, l.meets, true);
324                    out.boxes[l.b] = Some(PackedBox {
325                        node: plan.boxes[l.b].node.clone(),
326                        min,
327                        by: Provenance::Seam {
328                            seam: i,
329                            edge: s.edge.clone(),
330                            from: a.node,
331                            face: s.face,
332                        },
333                    });
334                    out.derived += 1;
335                    changed = true;
336                }
337                (None, Some(b)) => {
338                    let min = derive_corner(s.face, b.min, ext_a, ext_b, l.at, l.meets, false);
339                    out.boxes[l.a] = Some(PackedBox {
340                        node: plan.boxes[l.a].node.clone(),
341                        min,
342                        by: Provenance::Seam {
343                            seam: i,
344                            edge: s.edge.clone(),
345                            from: b.node,
346                            face: s.face,
347                        },
348                    });
349                    out.derived += 1;
350                    changed = true;
351                }
352                _ => {}
353            }
354        }
355        if !changed {
356            break;
357        }
358    }
359
360    // ---- every seam whose two ends stand: one placement, and the anchor
361    for (i, link) in links.iter().enumerate() {
362        let Some(l) = link else { continue };
363        let (Some(a), Some(b)) = (&out.boxes[l.a], &out.boxes[l.b]) else {
364            continue;
365        };
366        let s = &plan.seams[i];
367        let (ext_a, ext_b) = (ext_of(&plan.boxes[l.a]), ext_of(&plan.boxes[l.b]));
368        let want = derive_corner(s.face, a.min, ext_a, ext_b, l.at, l.meets, true);
369        if want != b.min {
370            d.push(two_placements(i, s, a, b, want));
371            out.refused.insert(i);
372            continue;
373        }
374        if let (Some(fa), Some(fb)) = (floors[l.a], floors[l.b]) {
375            out.seam_at[i] = Some(crossing_anchor(s.face, a.min, l.at, fa, fb));
376        }
377    }
378
379    // ---- components with nothing to place them
380    let mut parent: Vec<usize> = (0..plan.boxes.len()).collect();
381    fn find(p: &mut [usize], i: usize) -> usize {
382        let mut r = i;
383        while p[r] != r {
384            r = p[r];
385        }
386        let mut c = i;
387        while p[c] != r {
388            let n = p[c];
389            p[c] = r;
390            c = n;
391        }
392        r
393    }
394    for &(a, b) in &pairs {
395        let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
396        if ra != rb {
397            parent[ra.max(rb)] = ra.min(rb);
398        }
399    }
400    let mut members: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
401    for (i, b) in plan.boxes.iter().enumerate() {
402        if nodes.contains(b.node.0.as_str()) {
403            let r = find(&mut parent, i);
404            members.entry(r).or_default().push(i);
405        }
406    }
407    out.components = members.len();
408    let entry = graph.entry.0.as_str();
409    for (_, boxes) in members {
410        if boxes.iter().any(|&i| plan.boxes[i].min.is_some()) {
411            continue;
412        }
413        let names: Vec<String> = boxes
414            .iter()
415            .map(|&i| format!("`{}`", plan.boxes[i].node))
416            .collect();
417        let suggested = boxes
418            .iter()
419            .find(|&&i| plan.boxes[i].node.0 == entry)
420            .or(boxes.first())
421            .map(|&i| plan.boxes[i].node.to_string())
422            .unwrap_or_default();
423        d.push(Diagnostic::error(
424            DW_UNPLACED,
425            "site-plan",
426            format!("/content/boxes/{}", boxes[0]),
427            format!(
428                "nothing places {list}: no box among them pins its `min`, and a box stands \
429                 only where a pin puts it or where a seam hangs it off a box that already \
430                 stands. Pin one of them — `{suggested}` — with `\"min\": [x, z]`, and the \
431                 seams place the rest. {count} box(es) in this component.",
432                list = names.join(", "),
433                count = boxes.len(),
434            ),
435        ));
436    }
437    out
438}
439
440/// `DW0828`: an offset that is not a position on its own box's face.
441fn offset_problem(i: usize, s: &Seam, edge: &Edge, problem: OffsetProblem) -> Diagnostic {
442    let (which, detail) = match problem {
443        OffsetProblem::Shape { which } => (
444            which,
445            if s.face.is_horizontal_plane() {
446                format!(
447                    "the {face} face is a floor or ceiling with two in-plane axes, and `{which}` \
448                     gives one number. Write `[dx, dz]` — cells along x and z from the box's \
449                     low corner",
450                    face = s.face.as_str()
451                )
452            } else {
453                format!(
454                    "the {face} face is a wall with one horizontal axis, and `{which}` gives \
455                     two numbers. Write one — cells along the face from the box's low corner; \
456                     the sill is not written, it is the higher of the two floors",
457                    face = s.face.as_str()
458                )
459            },
460        ),
461        OffsetProblem::OffFace {
462            which,
463            axis,
464            value,
465            extent,
466            width,
467        } => (
468            which,
469            format!(
470                "`{which}` puts the crossing's corner at {value} along {axis} on a face that \
471                 runs 0..{last} — the crossing is {width} wide and the box is {extent} on that \
472                 axis. Write an offset on the face, or omit it and the crossing is centred; an \
473                 offset is never quietly clamped to fit",
474                last = extent - 1,
475            ),
476        ),
477    };
478    Diagnostic::error(
479        DW_SEAM_NOT_SHARED,
480        "site-plan",
481        format!("/content/seams/{i}/{which}"),
482        format!(
483            "the seam for `{id}` between `{an}` and `{bn}` names no position on the {side} \
484             box's face: {detail}.",
485            id = s.edge,
486            an = edge.a(),
487            bn = edge.b(),
488            side = if which == "at" { "`a`" } else { "`b`" },
489        ),
490    )
491}
492
493/// A seam whose two boxes both stand, and which would put `b` somewhere else:
494/// `DW0883` when a pin is one of the two authorities, `DW0828` when a loop does
495/// not close.
496fn two_placements(i: usize, s: &Seam, a: &PackedBox, b: &PackedBox, want: [i64; 2]) -> Diagnostic {
497    // Two authorities on `b`: its pin and this seam. When `b` was placed by
498    // another seam, the disagreement is between seams — a loop that does not
499    // close — however `a` came to stand where it stands.
500    let pinned = matches!(b.by, Provenance::Pinned);
501    let where_ = format!(
502        "`{an}` stands at [{ax}, {az}] ({a_by}); `{bn}` stands at [{bx}, {bz}] ({b_by}); hung \
503         off `{an}`'s {face} face by this seam, `{bn}` would stand at [{wx}, {wz}]",
504        an = a.node,
505        ax = a.min[0],
506        az = a.min[1],
507        a_by = a.by.describe(),
508        bn = b.node,
509        bx = b.min[0],
510        bz = b.min[1],
511        b_by = b.by.describe(),
512        face = s.face.as_str(),
513        wx = want[0],
514        wz = want[1],
515    );
516    if pinned {
517        Diagnostic::error(
518            DW_UNPLACED,
519            "site-plan",
520            format!("/content/seams/{i}"),
521            format!(
522                "two things place one box, and they disagree: {where_}. A pin is a claim the \
523                 packing verifies, never a second authority — move the pin to the corner the \
524                 seam derives, delete it and let the seam place the box, or change this seam's \
525                 `at`/`meets` so the two agree.",
526            ),
527        )
528    } else {
529        Diagnostic::error(
530            DW_SEAM_NOT_SHARED,
531            "site-plan",
532            format!("/content/seams/{i}"),
533            format!(
534                "the seam for `{id}` closes a loop, and the loop does not close: {where_}. Every \
535                 box in the loop was placed by an earlier seam, so this one can only check; \
536                 change its `at`/`meets` to where the two boxes really meet, or move the \
537                 offsets of the seams that placed them.",
538                id = s.edge,
539            ),
540        )
541    }
542}
543
544/// Every box's corner and how it was obtained, one line each — the derivation
545/// handed back, so a creator reads a corner from the build rather than typing
546/// it into the document.
547#[must_use]
548pub fn placements(c: &Campaign) -> Vec<String> {
549    let (Some(plan), Some(graph)) = (
550        c.site_plan.as_ref().map(|p| &p.content),
551        c.layout_graph.as_ref().map(|g| &g.content),
552    ) else {
553        return Vec::new();
554    };
555    let table = Metrics::table();
556    let mut reads = Reads::new();
557    let mut sink = Vec::new();
558    let (_, packed) = resolve(plan, graph, &table, &mut reads, &mut sink);
559    packed
560        .boxes
561        .iter()
562        .flatten()
563        .map(|b| {
564            format!(
565                "site-plan placing: `{node}` stands at [{x}, {z}] — {by}.",
566                node = b.node,
567                x = b.min[0],
568                z = b.min[1],
569                by = b.by.describe(),
570            )
571        })
572        .collect()
573}
574
575/// Resolve every box once: its footprint, its walk plane and its headroom. A
576/// floor naming a datum the plan does not declare is the ordinary
577/// dangling reference (`DW0112`) and the box is dropped, because a place with no
578/// plane has no geometry for any rule below to judge.
579/// The corners come from the packing (spec-0059 §3), which runs here so that
580/// every reader of the resolved plan — the checks, the derivation, the battery
581/// — holds one grid.
582pub(super) fn resolve<'a>(
583    plan: &'a SitePlanContent,
584    graph: &LayoutGraphContent,
585    table: &Metrics,
586    reads: &mut Reads,
587    d: &mut Vec<Diagnostic>,
588) -> (Vec<Placed<'a>>, Packed) {
589    let datums: BTreeMap<&str, i64> = plan.datums.iter().map(|x| (x.id.0.as_str(), x.y)).collect();
590    // Floors first: the packing needs them for every sill, and a box with no
591    // plane has no cells for any reader to work in.
592    let mut floors: Vec<Option<i64>> = Vec::with_capacity(plan.boxes.len());
593    for (i, b) in plan.boxes.iter().enumerate() {
594        floors.push(match &b.floor {
595            Floor::Y(y) => Some(*y),
596            Floor::Datum(id) => match datums.get(id.0.as_str()) {
597                Some(y) => Some(*y),
598                None => {
599                    d.push(Diagnostic::error(
600                        crate::codes::DANGLING_REF,
601                        "site-plan",
602                        format!("/content/boxes/{i}/floor"),
603                        format!(
604                            "box for `{node}` stands on `{id}`, which this plan declares no \
605                             `datums[]` entry for. Declare the plane, or give the box its own \
606                             `y` — a place with no plane has no walk surface, so nothing below \
607                             can say where it is.",
608                            node = b.node,
609                        ),
610                    ));
611                    None
612                }
613            },
614        });
615    }
616    let packed = pack(plan, graph, table, &floors, reads, d);
617    let mut out = Vec::new();
618    for (i, b) in plan.boxes.iter().enumerate() {
619        let (Some(floor), Some(pb)) = (floors[i], &packed.boxes[i]) else {
620            continue; // `DW0112` or `DW0883` said why this box has no cells.
621        };
622        let clearance = match b.ceiling {
623            Ceiling::Clearance(c) => c.get(),
624            // A sky-open place claims exactly the courses of air it declares
625            // and nothing above them.
626            Ceiling::Open(n) => n.get(),
627        };
628        out.push(Placed {
629            index: i,
630            plan: b,
631            foot: [
632                pb.min[0],
633                pb.min[0] + i64::from(b.extent[0].get()) - 1,
634                pb.min[1],
635                pb.min[1] + i64::from(b.extent[1].get()) - 1,
636            ],
637            floor,
638            clearance,
639            by: pb.by.clone(),
640        });
641    }
642    (out, packed)
643}