1use super::*;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Provenance {
13 Pinned,
15 Seam {
17 seam: usize,
19 edge: EdgeId,
21 from: NodeId,
23 face: Face,
26 },
27}
28
29impl Provenance {
30 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#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct PackedBox {
47 pub node: NodeId,
49 pub min: [i64; 2],
51 pub by: Provenance,
53}
54
55#[derive(Debug, Default)]
58pub(super) struct Packed {
59 boxes: Vec<Option<PackedBox>>,
61 pub(super) seam_at: Vec<Option<[i64; 2]>>,
65 pub(super) refused: BTreeSet<usize>,
67 pub(super) components: usize,
69 pub(super) pinned: usize,
70 pub(super) derived: usize,
71}
72
73#[derive(Debug, Clone, Copy)]
76struct Link {
77 a: usize,
78 b: usize,
79 at: [i64; 2],
80 meets: [i64; 2],
81}
82
83fn 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), }
91}
92
93fn 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
109enum OffsetProblem {
111 Shape { which: &'static str },
114 OffFace {
116 which: &'static str,
117 axis: &'static str,
118 value: i64,
119 extent: i64,
120 width: i64,
121 },
122}
123
124fn 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 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 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 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
178fn 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
217fn 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
239fn 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 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 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; };
287 if !edge.has_seam() {
288 links.push(None);
289 continue; }
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; };
298 pairs.push((a, b));
299 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 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 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 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
440fn 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
493fn two_placements(i: usize, s: &Seam, a: &PackedBox, b: &PackedBox, want: [i64; 2]) -> Diagnostic {
497 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#[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
575pub(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 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; };
622 let clearance = match b.ceiling {
623 Ceiling::Clearance(c) => c.get(),
624 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}