delvewright_dsl/siteplan/mod.rs
1//! **The whole owns the space and hands out boxes** (spec-0049 §4) — pipeline
2//! stage 4, the geometric embedding of the layout graph.
3//!
4//! One campaign stage document, `site-plan.json`: the whole map's design of
5//! record. It says where the region is, which plane each place stands on, what
6//! footprint each place gets, where two places connect and through what opening,
7//! what mass the whole itself owns, and which of the brief's numbers the plan is
8//! held to.
9//!
10//! Everything here is decided **upstream of any geometry**. No block exists yet;
11//! nothing in this module reads one. What is being judged is whether the plan is
12//! a plan — whether the boxes fit in the region and not in each other, whether
13//! two places that claim to connect really touch, and whether the numbers the
14//! brief fixed still hold once the boxes are drawn.
15//!
16//! # Extent flows down, and it is unrepresentable for it to flow up
17//!
18//! The reset this stage answers was caused by parts choosing their own size and
19//! the whole becoming whatever they added up to. So [`SitePlanContent::region`]
20//! is a **required field with no derived spelling**: there is no
21//! `"region": "fit"`, no default, and no constructor anywhere that computes one
22//! from the boxes. A plan cannot state its extent as a consequence; it can only
23//! state it, and `DW0826` then refuses a box that does not fit, naming the box
24//! rather than the region. That is not a check — it is the absence of a way to
25//! write the other thing.
26//!
27//! # Seams are allocated, not discovered
28//!
29//! A seam is placed by the plan, on a face the two boxes already share, at cells
30//! the plan names. Two places therefore connect **by construction**: the
31//! two-pieces-cannot-mate failure is resolved here, where both boxes are still
32//! free to move, and never later between two finished buildings. `DW0828` and
33//! `DW0829` are what make the allocation real rather than a claim.
34//!
35//! # One authority per fact
36//!
37//! Three places where the obvious shape would have carried two:
38//!
39//! * A box's floor is its **datum** and nothing else. The spec's `min` carried a
40//! `y` beside the declared floor, which is two numbers for one plane and no
41//! rule about which wins; here a box is a footprint (`min`/`extent` on `x`
42//! and `z`) standing at a [`Floor`]. §9 records the departure.
43//! * A seam's **rise is derived** from the two boxes' floors. Authoring it would
44//! be authoring arithmetic — unlike the layout graph's `critical_path`, which
45//! is authored precisely because it is a *choice* among many, a rise is the
46//! consequence of where the plan already put the two places. §9 records it.
47//! * The plan's `lighting` is [`crate::AreaLighting`], the engine's
48//! existing "which fixture, to what light level" object, not a twin of it.
49//!
50//! # No opt-out exists
51//!
52//! Not one check here has an acknowledgement, an override or an exemption
53//! field. That is deliberate and it is the cheapest possible answer to
54//! `CLAUDE.md`'s question of every escape hatch — *could the defect this hatch
55//! exists to catch supply the hatch's own proof obligation?* — because a hatch
56//! that does not exist cannot be supplied.
57//!
58//! Determinism (ADR-0006): every set and map is a `BTreeSet`/`BTreeMap` and
59//! every walk is over a slice in document order.
60
61use std::collections::{BTreeMap, BTreeSet};
62use std::num::NonZeroU32;
63
64use schemars::JsonSchema;
65use serde::{Deserialize, Serialize};
66
67use crate::AreaLighting;
68use crate::diagnostic::{Diagnostic, DwCode, ExitTier};
69use crate::envelope::Campaign;
70use crate::ids::{DatumId, EdgeId, FactId, NodeId, ViewId, VolumeId};
71use crate::layout::{Edge, LayoutGraphContent, StationKind};
72use crate::metrics::{
73 MAX_JUMP_RISE_16, MetricKind, MetricValue, Metrics, Pitch, Reads, passable_clearance_cells,
74 passable_width_cells,
75};
76
77mod check;
78mod claim;
79mod fillcheck;
80mod ground;
81mod measure;
82mod pack;
83mod place;
84mod region;
85mod seam;
86
87pub use check::*;
88pub use claim::*;
89pub use ground::*;
90use measure::*;
91pub use pack::*;
92pub use place::*;
93use region::*;
94pub use seam::*;
95
96crate::dw_code! {
97 /// `DW0824`: the graph and the plan do not agree exactly.
98 pub const DW_PLAN_AGREEMENT: DwCode = DwCode::new("DW0824", ExitTier::Build);
99}
100
101crate::dw_code! {
102 /// `DW0826`: a box leaves the region.
103 pub const DW_BOX_LEAVES_REGION: DwCode = DwCode::new("DW0826", ExitTier::Build);
104}
105
106crate::dw_code! {
107 /// `DW0827`: two boxes overlap.
108 pub const DW_BOXES_OVERLAP: DwCode = DwCode::new("DW0827", ExitTier::Build);
109}
110
111crate::dw_code! {
112 /// `DW0828`: a seam is not on a shared face.
113 pub const DW_SEAM_NOT_SHARED: DwCode = DwCode::new("DW0828", ExitTier::Build);
114}
115
116/// **How many cells stand between two connected boxes: the wall they share.**
117///
118/// The one number every geometric check in this stage is written against, and
119/// the one an author has to know before the first box goes down. A box is the
120/// **play space** of a place — the cells a body can be in — so the shell is not
121/// inside it: it stands in this gap, and two places that connect leave exactly
122/// this much room for it. Boxes placed flush have no wall to cut a seam through
123/// and `DW0828` refuses them.
124///
125/// It is a constant rather than a literal because the authoring documents state
126/// it: [`PlanBox`]'s schema description carries this value, and
127/// `crates/dsl/tests/v14_site_plan.rs` asserts the exported description against
128/// this constant, so the rule a person reads and the rule the checks enforce
129/// cannot drift apart.
130pub const SHARED_FACE_GAP_CELLS: i64 = 1;
131
132crate::dw_code! {
133 /// `DW0829`: a seam's opening is not a standard, or does not fit.
134 pub const DW_SEAM_OPENING: DwCode = DwCode::new("DW0829", ExitTier::Build);
135}
136
137crate::dw_code! {
138 /// `DW0992`: a climb that climbs nothing (spec-0098 §2c).
139 pub const DW_CLIMB_RISES_NOTHING: DwCode = DwCode::new("DW0992", ExitTier::Build);
140}
141
142crate::dw_code! {
143 /// `DW0830`: a stair seam cannot be built at standard pitch.
144 pub const DW_STAIR_PITCH: DwCode = DwCode::new("DW0830", ExitTier::Build);
145}
146
147crate::dw_code! {
148 /// `DW0831`: a drop seam falls the wrong way, past the survivable fall, or
149 /// past the plan's declared `max_drop`.
150 pub const DW_DROP_POLICY: DwCode = DwCode::new("DW0831", ExitTier::Build);
151}
152
153crate::dw_code! {
154 /// `DW0876`: a seam does not declare a connection this engine builds
155 /// (spec-0053 §6).
156 ///
157 /// **One code, three shapes of one claim** — the claim being that this seam
158 /// states a crossing the derivation can build and the observer can measure:
159 ///
160 /// 1. it declares neither an `opening` nor a `contact`, or both;
161 /// 2. its contact's span leaves the shared face `DW0828` established;
162 /// 3. it is a contact on a `stair`, `barred` or `vision` connection.
163 ///
164 /// They are one code rather than three because the author's next action is
165 /// the same in every case — say which kind of hand-off this is and give it a
166 /// shape the engine has — and because a seam exhibiting one of them has no
167 /// crossing for any rule below to judge. A contact's width is the author's:
168 /// a front one cell wide is as legal as one fifty-five wide.
169 pub const DW_CONTACT: DwCode = DwCode::new("DW0876", ExitTier::Build);
170}
171
172crate::dw_code! {
173 /// `DW0833`: a brief identity does not hold.
174 pub const DW_IDENTITY_FALSE: DwCode = DwCode::new("DW0833", ExitTier::Build);
175}
176
177crate::dw_code! {
178 /// `DW0834`: the identity gate binds nothing. Warning — see [`identities`].
179 pub const DW_IDENTITY_EMPTY: DwCode = DwCode::new("DW0834", ExitTier::Build);
180}
181
182crate::dw_code! {
183 /// `DW0835`: a whole-owned volume enters a box.
184 pub const DW_VOLUME_IN_BOX: DwCode = DwCode::new("DW0835", ExitTier::Build);
185}
186
187crate::dw_code! {
188 /// `DW0839`: two placement authorities in one campaign — a `site-plan.json` and
189 /// a non-empty `areas[]` both present.
190 pub const DW_TWO_AUTHORITIES: DwCode = DwCode::new("DW0839", ExitTier::Build);
191}
192
193crate::dw_code! {
194 /// `DW0988`: a roof the plan has no room for (spec-0098 §7) — declared on
195 /// a sky-open box, or rising into another place.
196 pub const DW_ROOF_NO_ROOM: DwCode = DwCode::new("DW0988", ExitTier::Build);
197}
198
199crate::dw_code! {
200 /// `DW0883`: a box is not placed exactly once (spec-0059 §5). Two shapes of one
201 /// claim: a connected component of the seam graph in which no box is pinned, so
202 /// nothing places it; and a pinned box the packing also reaches, at a different
203 /// corner, so two things place it.
204 pub const DW_UNPLACED: DwCode = DwCode::new("DW0883", ExitTier::Build);
205}
206
207// ---------------------------------------------------------------------------
208// The vocabulary the derivation synthesizes (spec-0049 §5.2)
209// ---------------------------------------------------------------------------
210
211/// **The one area a site-plan campaign has.**
212///
213/// A campaign places its pieces either with `areas[]` or with a site plan, never
214/// both (`DW0839`), so a site-plan campaign has exactly one place for an NPC to
215/// stand in and one area for a quest to belong to. The name is fixed rather than
216/// authored because there is nothing to choose: the site plan is the whole map,
217/// and a second name for it would be a second way to spell one thing.
218pub const SITE_AREA: &str = "area/site";
219
220/// The anchor name the campaign's **entry** stands under.
221///
222/// A *name*, and only a name: what makes this anchor the entry is the declared
223/// entry **role** (spec-0046) the derivation gives it, which is the one thing
224/// the compiler's resolution consults. The spelling survives because a
225/// site-plan campaign's quests and NPCs may address the entry cell like any
226/// other anchor, and `spawn` is the word the rest of the vocabulary already
227/// uses; nothing resolves through it.
228pub const ENTRY_ANCHOR: &str = "spawn";
229
230/// The anchor at a place's floor centre — where quests, NPCs and waves in a
231/// site-plan campaign stand.
232///
233/// `node/near-hall` becomes `anchor/node-near-hall`, and the reshaping is not
234/// cosmetic: a campaign reaches an anchor through [`crate::ids::AnchorId`],
235/// which is `anchor/<kebab>`, so `node/<id>` — spec-0049 §5.2's spelling — is
236/// not a name any document could write. The three families (`node-`, `seam-`,
237/// `unlock-`) are disjoint by their first segment, so no two synthesized
238/// anchors can collide however the graph is named.
239#[must_use]
240pub fn node_anchor(node: &NodeId) -> String {
241 format!("anchor/node-{}", slug(node.0.as_str()))
242}
243
244/// The gate region over a `barred` seam's opening — what an `open-gate` or a
245/// `shortcut` names.
246#[must_use]
247pub fn seam_anchor(edge: &EdgeId) -> String {
248 format!("anchor/seam-{}", slug(edge.0.as_str()))
249}
250
251/// The anchor on the openable side of a one-sided `barred` seam, where a
252/// shortcut's far-side affordance stands.
253#[must_use]
254pub fn seam_unlock_anchor(edge: &EdgeId) -> String {
255 format!("anchor/unlock-{}", slug(edge.0.as_str()))
256}
257
258/// The part of an id after its kind prefix.
259fn slug(id: &str) -> &str {
260 id.split_once('/').map_or(id, |(_, rest)| rest)
261}
262
263/// **What a sealed `barred` seam stands in until content opens it.**
264///
265/// One definition, here rather than in the derivation that lays it, for the same
266/// structural reason the metrics table owns the nav model's constants: two
267/// parties need this block and they need the *same* one. The derivation writes it
268/// into the gate region and declares it on the synthesized gate anchor; every
269/// verb that needs a gate's fill block — `close-gate`, a `shortcut`'s clear, a
270/// `timed-gate`'s clock — asks [`synthesized_gate_block`] whether this campaign
271/// declares one. A copy in each place would be an agreement rather than a fact.
272pub const SEAM_BAR: &str = "minecraft:iron_bars";
273
274/// The fill block a **synthesized** gate anchor declares, or `None` when `anchor`
275/// is not one of this campaign's derived seam gates.
276///
277/// This exists because `DW0343`'s question — *can the compiler fill and clear
278/// this gate?* — used to be answered by one instrument only, the prefab registry,
279/// and a derived world has no prefab. The answer came back honest and about a
280/// smaller world than the campaign has: a `shortcut` naming the very
281/// `anchor/seam-<edge>` the derivation seals with [`SEAM_BAR`] was refused for
282/// declaring no fill block, while the block sat in the derivation's own
283/// `AnchorSpec::Gate`. Nothing was red, because the check was refusing content.
284///
285/// `None` for a campaign with no site plan, and for any anchor the derivation
286/// does not synthesize — those are the prefab registry's to answer for, and this
287/// function never overrides it.
288#[must_use]
289pub fn synthesized_gate_block(c: &Campaign, anchor: &str) -> Option<&'static str> {
290 // Asks the ONE kind authority rather than re-walking the edges, and that is
291 // the whole repair: this used to enumerate `Edge::Barred` alone, so a
292 // `close-gate`, `shortcut` or `timed-gate` naming a **gate station**
293 // (spec-0052) would have been refused by `DW0343` for declaring no fill
294 // block — a refusal whose message says "declare the gate on an anchor of a
295 // piece an area binds", which a site-plan campaign cannot do at all
296 // (`DW0839` refuses a campaign that carries both `areas[]` and a plan).
297 // A narrow binding on the general mechanism, reading as a missing feature.
298 matches!(
299 synthesized_anchor_kinds(c).get(anchor),
300 Some(StationKind::Gate)
301 )
302 .then_some(SEAM_BAR)
303}
304
305/// **Every anchor a site-plan campaign's blockout provides, and what SHAPE each
306/// one is** — the single authority behind [`synthesized_anchors`].
307///
308/// The kind travels with the name because a kind is a property of the **anchor**,
309/// not of the verb that first needed one: `synthesized_gate_block` needed to know
310/// whether a name was a gate and answered by privately re-walking the edges, and
311/// a second consumer wanting the same fact would have re-walked them again. One
312/// function answers it, and everything that needs a shape asks here.
313///
314/// The mapping, and it is total:
315///
316/// * [`ENTRY_ANCHOR`] and every reached place's `anchor/node-…` —
317/// [`StationKind::Point`], the floor centre a body stands on. Scenery
318/// (`reached: false`) has none.
319/// * every `anchor/unlock-…` — [`StationKind::Point`], where the shortcut's
320/// far-side affordance stands.
321/// * every `anchor/seam-…` — [`StationKind::Gate`], the region the derivation
322/// fills with [`SEAM_BAR`] and content opens.
323/// * every declared station — the kind its node declared (spec-0052 §3).
324///
325/// Empty for a campaign with no site plan — it has prefabs instead, and their
326/// metadata is the authority.
327#[must_use]
328pub fn synthesized_anchor_kinds(c: &Campaign) -> BTreeMap<String, StationKind> {
329 let mut out: BTreeMap<String, StationKind> = BTreeMap::new();
330 if c.site_plan.is_none() {
331 return out;
332 }
333 let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
334 return out; // `DW0824` refused the plan; there is nothing to name.
335 };
336 out.insert(ENTRY_ANCHOR.to_string(), StationKind::Point);
337 for n in &graph.nodes {
338 // Scenery (`reached: false`, spec-0098 §14) has no floor a body stands
339 // on, so it has no place anchor: a proof that floods from anchors
340 // would otherwise flood from inside a place nothing enters, and a
341 // document naming one is refused where it is written.
342 if n.reached {
343 out.insert(node_anchor(&n.id), StationKind::Point);
344 }
345 // A station whose name collides with a synthesized one is `DW0869`, and
346 // one that collides with another station is `DW0870`; both are errors,
347 // so this insert never silently reinterprets a name a campaign builds
348 // with. Inserting anyway keeps the set EXACT for the refused document
349 // too, which is what lets the kind check name the declared kind rather
350 // than a shape the author did not write.
351 for s in &n.stations {
352 out.insert(s.anchor.as_str().to_string(), s.kind);
353 }
354 }
355 for e in &graph.edges {
356 let Edge::Barred { id, opens_from, .. } = e else {
357 continue;
358 };
359 out.insert(seam_anchor(id), StationKind::Gate);
360 if !matches!(opens_from, crate::layout::OpensFrom::Either) {
361 out.insert(seam_unlock_anchor(id), StationKind::Point);
362 }
363 }
364 out
365}
366
367/// **Every anchor a site-plan campaign's blockout provides**, derived from the
368/// documents alone.
369///
370/// One authority, and that is why it lives here rather than in the derivation
371/// that places them: validation resolves a campaign's anchor references against
372/// this set, the derivation creates exactly these anchors, and a name that
373/// validated could therefore never fail to exist at build time. Two functions
374/// agreeing about the spelling is the drift this one removes.
375///
376/// Empty for a campaign with no site plan — it has prefabs instead, and their
377/// metadata is the authority.
378#[must_use]
379pub fn synthesized_anchors(c: &Campaign) -> BTreeSet<String> {
380 // The names ARE the keys of the kind table, taken rather than re-derived:
381 // two functions walking the graph and agreeing about the spelling is the
382 // exact drift this module's note exists to remove, and a station made the
383 // walk long enough that a second copy would eventually diverge.
384 synthesized_anchor_kinds(c).into_keys().collect()
385}
386
387/// **The synthesized names one PLACE owes** (spec-0050 §6) — the subset of
388/// [`synthesized_anchors`] whose bearer is this box.
389///
390/// Its own `anchor/node-…`; [`ENTRY_ANCHOR`] when it is the entry node; each
391/// `anchor/unlock-…` whose `opens_from` side it is — the side the derivation
392/// stands that affordance in; and each `barred` seam's gate region
393/// (`anchor/seam-…`) whose plane this place owns (spec-0098 §2), because the
394/// piece that owns a plane ships the gate standing in it.
395///
396/// Here rather than in `crate::detailplan` because the answer is a fact about
397/// the graph, and [`synthesized_anchors`] is the one authority for what a
398/// site-plan campaign provides — a second module deciding which names belong to
399/// which place is exactly the two-functions-agreeing-about-spelling drift that
400/// note exists to remove.
401///
402/// `crates/delvec/tests/blockout.rs`'s
403/// `the_owed_anchors_partition_the_synthesized_set` proves the two PARTITION
404/// rather than merely overlap: every synthesized name is owed by exactly one
405/// place or is a gate region no place owes. A name in neither would be one a
406/// campaign resolves and no piece is ever asked for; a name in both would be two
407/// pieces claiming one anchor.
408///
409/// Empty for a campaign with no site plan, and for a node the graph does not
410/// have.
411#[must_use]
412pub fn owed_anchors(c: &Campaign, node: &NodeId) -> BTreeSet<String> {
413 let mut out: BTreeSet<String> = BTreeSet::new();
414 if c.site_plan.is_none() {
415 return out;
416 }
417 let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
418 return out; // `DW0824` refused the plan; there is nothing to name.
419 };
420 let Some(n) = graph.nodes.iter().find(|n| &n.id == node) else {
421 return out; // `DW0842` names a row whose place the graph does not have.
422 };
423 // Scenery (`reached: false`) owes no place to stand: nothing visits it
424 // (spec-0098 §14).
425 if n.reached {
426 out.insert(node_anchor(node));
427 }
428 if &graph.entry == node {
429 out.insert(ENTRY_ANCHOR.to_string());
430 }
431 // Every station of this node (spec-0052 §6). The owed set grows **upstream**,
432 // and that one widening is what carries the whole binding chain: the
433 // `detail-plan` `anchors` map must now bind each of them, and it still
434 // refuses every key outside this set, so a binding cannot invent vocabulary
435 // and a typo cannot pass as intent.
436 for s in &n.stations {
437 out.insert(s.anchor.as_str().to_string());
438 }
439 // The gate region over a `barred` seam is owed by the place that owns the
440 // plane it stands in (spec-0098 §2): every seam lies in a plane some place
441 // owns, so its shut state is that piece's to ship.
442 let resolved = SitePlan::of(c);
443 let site = resolved.site();
444 for s in &resolved.seams {
445 if s.class != "barred" || (&s.a != node && &s.b != node) {
446 continue;
447 }
448 if site.owner(s.opening.0) == Owner::Place(node.clone()) {
449 out.insert(seam_anchor(&s.edge));
450 }
451 }
452 for e in &graph.edges {
453 let Edge::Barred { id, opens_from, .. } = e else {
454 continue;
455 };
456 let side = match opens_from {
457 crate::layout::OpensFrom::A => e.a(),
458 crate::layout::OpensFrom::B => e.b(),
459 crate::layout::OpensFrom::Either => continue,
460 };
461 if side == node {
462 out.insert(seam_unlock_anchor(id));
463 }
464 }
465 out
466}
467
468// ---------------------------------------------------------------------------
469// The document (spec-0049 §4.1)
470// ---------------------------------------------------------------------------
471
472/// The `site-plan` stage document's payload: the geometric embedding of the
473/// layout graph, and the whole map's design of record.
474#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
475#[serde(deny_unknown_fields)]
476pub struct SitePlanContent {
477 /// **The whole map's one region, in world coordinates.**
478 ///
479 /// Required, with no way to omit it and no way to derive it: the schema has
480 /// no "compute this from the boxes" spelling, so extent-flows-up is
481 /// unrepresentable rather than merely forbidden. The number comes from the
482 /// geometry brief and the identities hold the plan to it.
483 ///
484 /// The water plane is deliberately **not** site-plan surface: `horizon:
485 /// ocean` in the stage-1 world document already fixes sea level, and the
486 /// plan reads that single authority rather than restating it.
487 pub region: WorldBox,
488 /// Named ground planes the boxes stand on.
489 #[serde(default, skip_serializing_if = "Vec::is_empty")]
490 pub datums: Vec<Datum>,
491 /// **Exactly one box per graph node** (`DW0824`).
492 pub boxes: Vec<PlanBox>,
493 /// **Exactly one seam per traversal edge** (`DW0824`). A `vision` edge
494 /// carries a [`Sightline`] instead — see [`Sightline`] for why.
495 pub seams: Vec<Seam>,
496 /// The mass the WHOLE owns: the mountain a cave system is inside, the ground
497 /// under a village, the sky a silhouette needs kept empty.
498 #[serde(default, skip_serializing_if = "Vec::is_empty")]
499 pub volumes: Vec<Volume>,
500 /// The guarded comparisons binding this plan to the geometry brief's facts.
501 #[serde(default, skip_serializing_if = "Vec::is_empty")]
502 pub identities: Vec<Identity>,
503 /// One per `vision` edge (`DW0824`).
504 #[serde(default, skip_serializing_if = "Vec::is_empty")]
505 pub sightlines: Vec<Sightline>,
506 /// The named exterior vantages the walk judges the silhouette from. Optional;
507 /// a plan with zero views has that zero stated in the binding line.
508 #[serde(default, skip_serializing_if = "Vec::is_empty")]
509 pub views: Vec<View>,
510 /// One lighting setting applied to every enclosed box, so a blockout
511 /// interior is walkable at night without per-box surface.
512 ///
513 /// **The engine's existing object**, not a twin of it: [`AreaLighting`] is
514 /// already "which fixture, to what light level", and the relight pass that
515 /// consumes it is the same pass either way. A second two-field struct here
516 /// would be the private-copy defect `CLAUDE.md` names, and it would fork the
517 /// range check the moment one of them grew a third field.
518 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub lighting: Option<AreaLighting>,
520 /// **What every cell no place claims and no volume covers becomes**
521 /// (spec-0098 §2b): `solid` rock for an enclosed site (a dungeon, a cave),
522 /// `open` ground under sky over a declared terrain for an open one (a town).
523 ///
524 /// Required, with no default: either default would be a judgement about
525 /// what kind of site this is, and that judgement is the author's. Per
526 /// region it is overridden by `volumes[]`, exactly as before.
527 pub fill: Fill,
528 /// **The deepest fall a designed drop in this plan may take**, in blocks —
529 /// the author's own policy, when they have one (`DW0831` confirms every
530 /// drop seam falls no further). Absent, no policy cap applies; the
531 /// survivable fall of an unarmoured body holds every drop either way.
532 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub max_drop: Option<NonZeroU32>,
534}
535
536/// What undeclared space becomes (spec-0098 §2b).
537#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
538#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
539pub enum Fill {
540 /// Every unclaimed cell holds `block` — the enclosed site, whose places are
541 /// carved out of rock.
542 Solid {
543 /// The block state the rock is.
544 block: String,
545 },
546 /// A natural ground surface under sky: at the terrain's height the
547 /// `surface` block, under it `below`, above it air.
548 Open {
549 /// The ground's shape: one flat height, or a heightmap.
550 terrain: Terrain,
551 /// The block state of the top course.
552 surface: String,
553 /// The block state under the top course.
554 below: String,
555 },
556}
557
558/// **The site's terrain: a declared heightfield** (spec-0098 §2c).
559///
560/// Every height here is the `y` of the **surface block** — the ground's top
561/// cell — so a body walks one above it.
562#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
563#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
564pub enum Terrain {
565 /// One height everywhere: the named datum is the terrain's walk plane, so
566 /// the surface block stands at the datum's `y − 1` — exactly where a box
567 /// standing on that datum has its floor course.
568 Flat {
569 /// The plane (`DW0112` if the plan declares no such datum).
570 datum: DatumId,
571 },
572 /// A greyscale image exactly the region's `x × z` pixels, in the campaign:
573 /// pixel `(px, pz)` is column `(region.min.x + px, region.min.z + pz)`,
574 /// and its surface block stands at `base_y + value × range / 255` (integer
575 /// division). The creator's own artifact — drawn, or generated by a tool
576 /// whose seed is written down.
577 Heightmap {
578 /// The image's campaign-relative path.
579 heightmap: String,
580 /// The surface `y` a black pixel stands for.
581 base_y: i64,
582 /// How many blocks a white pixel stands above `base_y`.
583 range: u32,
584 },
585}
586
587/// A box of world cells: its low corner and its extent, in blocks.
588///
589/// The cells are `min[i] ..= min[i] + extent[i] - 1` on each axis. The extent is
590/// [`NonZeroU32`] rather than a `u32` a check refuses: a zero-extent region is
591/// not a small region, it is a document that does not describe a volume, and the
592/// schema says so (`minimum: 1`) so the parse refuses it as an ordinary
593/// `DW0100`. One fewer diagnostic to write and one fewer to forget.
594#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
595#[serde(deny_unknown_fields)]
596pub struct WorldBox {
597 /// Low corner `[x, y, z]`, in world coordinates.
598 pub min: [i64; 3],
599 /// Extent `[dx, dy, dz]`, in blocks.
600 pub extent: [NonZeroU32; 3],
601}
602
603impl WorldBox {
604 /// The high corner (inclusive).
605 #[must_use]
606 pub fn max(&self) -> [i64; 3] {
607 [
608 self.min[0] + i64::from(self.extent[0].get()) - 1,
609 self.min[1] + i64::from(self.extent[1].get()) - 1,
610 self.min[2] + i64::from(self.extent[2].get()) - 1,
611 ]
612 }
613}
614
615/// A named ground plane a box's floor sits on.
616#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
617#[serde(deny_unknown_fields)]
618pub struct Datum {
619 /// Datum id (`datum/<kebab>`), unique within the plan.
620 pub id: DatumId,
621 /// The world `y` of the walk plane.
622 pub y: i64,
623 /// What this plane is, for a reader of the plan.
624 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub note: Option<String>,
626}
627
628/// Where a place's walk plane is.
629///
630/// Two spellings of one number, and the second is not redundant: a plane several
631/// places stand on is named once as a [`Datum`] and moved once, while a place
632/// that stands alone at its own height has no plane to name. An `identities[]`
633/// entry can only bind to a *named* one, which is the pressure toward naming.
634#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
635#[serde(rename_all = "kebab-case", deny_unknown_fields)]
636pub enum Floor {
637 /// A plane the plan names (`DW0112` if the plan declares no such datum).
638 Datum(DatumId),
639 /// A world `y` this place alone stands at.
640 Y(i64),
641}
642
643/// What closes a place overhead.
644#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
645#[serde(rename_all = "kebab-case", deny_unknown_fields)]
646pub enum Ceiling {
647 /// Cells of headroom over the walk plane. A body's feet are at the floor and
648 /// the ceiling course sits at `floor + clearance`.
649 Clearance(NonZeroU32),
650 /// A sky-open place — a courtyard, a shore, a summit, a bridge's deck:
651 /// exactly this many courses of air over the walk plane are claimed, and
652 /// **nothing above them**. An open place is precisely one that makes no
653 /// claim on the air over its headroom, so a `clearance` volume above a
654 /// courtyard is the whole reserving sky rather than two authorities over
655 /// one cell, and a place hung over the yard stands in the yard's sky.
656 Open(NonZeroU32),
657}
658
659/// What a place stands on (spec-0098 §2).
660///
661/// The third declared plane of a box's cuboid, beside [`PlanBox::floor`] (the
662/// walk plane) and [`PlanBox::ceiling`] (the headroom).
663#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
664#[serde(rename_all = "kebab-case", deny_unknown_fields)]
665pub enum Base {
666 /// The place stands on the site's ground: its claim reaches down, column
667 /// by column, to the terrain under its plot or its floor course, whichever
668 /// is lower, and the whole hands it that ground and fixes its ring.
669 #[default]
670 Ground,
671 /// The place hangs — a bridge, a gantry, a treehouse: its claim stops this
672 /// many courses under its floor course (`0` is the floor course alone). It
673 /// is handed no ground and no fixed ring, and the space under it is
674 /// whoever claims it, else the site's fill. Terrain reaching into the claim
675 /// is refused (`DW0990`).
676 Aloft(u32),
677}
678
679impl Base {
680 /// The underside courses under the floor course, on an aloft place.
681 #[must_use]
682 pub fn aloft(self) -> Option<u32> {
683 match self {
684 Base::Ground => None,
685 Base::Aloft(n) => Some(n),
686 }
687 }
688
689 /// True on the default, so a plan that does not state it does not print it.
690 #[must_use]
691 pub fn is_ground(&self) -> bool {
692 matches!(self, Base::Ground)
693 }
694}
695
696/// One place, embedded: a cuboid of the world.
697///
698/// **A box is a 3D region.** Its `min`/`extent` are the two horizontal axes;
699/// its vertical extent is three declared planes — [`PlanBox::floor`], the one
700/// authority for the walk plane; [`PlanBox::ceiling`], the headroom over it
701/// (a lid, or exactly `n` courses of sky); and [`PlanBox::base`], what it
702/// stands on (the site's ground, or `n` underside courses hung in the air).
703/// Two places conflict only where their cuboids overlap (`DW0827`); a cell no
704/// place claims is the whole's — on an `open` site, walkable ground and sky.
705///
706/// **A box is the PLAY SPACE, and connected boxes are separated by exactly one
707/// cell.** `extent` is the interior a body can stand in; the shell the blockout
708/// derivation builds is not inside it. That shell stands in the one-cell gap
709/// between two neighbours, and on the course under the floor and over the
710/// ceiling. So two places that connect are placed one cell apart on the face
711/// they share — that cell is the wall they have in common, written once — and
712/// two boxes placed flush have no wall for a seam to be cut through, which
713/// `DW0828` refuses. Worked: a box at `min: [4, 4]` with `extent: [4, 4]`
714/// occupies x 4..7, so its eastern neighbour's `min` x is 9, never 8.
715///
716/// The consequence is what makes the checks say what they look like they say:
717/// `extent` is the play space the author declared, and a plan never states a
718/// wall's thickness anywhere, because the gap is where the wall is.
719#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
720#[serde(deny_unknown_fields)]
721pub struct PlanBox {
722 /// The graph node this box embeds.
723 ///
724 /// **The ordering tooth, at the type level**: there is no way to write a box
725 /// that does not name a place, so a site plan cannot describe a space the
726 /// layout graph has not declared (spec-0049 §7.1).
727 pub node: NodeId,
728 /// **A pin**: the low corner `[x, z]` in world coordinates, when the author
729 /// chooses where this box stands. Optional (spec-0059 §2): a box with no pin
730 /// stands where the packing puts it — one cell beyond the face of the box
731 /// its first seam in document order hangs it off. At least one box of every
732 /// connected component of the seam graph is pinned, or nothing places the
733 /// component (`DW0883`); a pinned box the packing also reaches at a
734 /// different corner is refused naming both (`DW0883`). Two horizontal
735 /// numbers, never three — the vertical position is `floor`.
736 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub min: Option<[i64; 2]>,
738 /// Interior footprint `[dx, dz]`, in blocks — any whole number on either axis.
739 /// Two horizontal numbers, never three — the vertical size is `ceiling`.
740 ///
741 /// **This is play space, not the building.** The box covers `min` to
742 /// `min + extent - 1` inclusive, and the walls stand outside it, in the
743 /// one-cell gap that separates connected places (`DW0828`).
744 pub extent: [NonZeroU32; 2],
745 /// The walk plane.
746 pub floor: Floor,
747 /// What closes it overhead.
748 pub ceiling: Ceiling,
749 /// What it stands on: `"ground"` (the default) or `{"aloft": n}`.
750 #[serde(default, skip_serializing_if = "Base::is_ground")]
751 pub base: Base,
752 /// **The sky this place stands under from the first tick** (spec-0080
753 /// §3.2): one of `world.atmospheres[]`, painted at world setup over the
754 /// box's play space grown as far as the client's biome blend reads — the
755 /// same capability `areas[].atmosphere` is, on the
756 /// other class of place with a world box. Absent: the horizon's biome.
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub atmosphere: Option<crate::ids::AtmosphereId>,
759 /// **The roof the whole reserves over this place** (spec-0098 §3): how
760 /// many courses it rises above the ceiling course and how far it overhangs
761 /// the shell on each horizontal side. Massed solid at stage 5 so a walker
762 /// sees the volume the building will take; drawn by the place's own piece
763 /// once detailed. Absent: a flat lid one course thick, which the piece owns
764 /// too. Refused on a sky-open box, and where its courses rise into another
765 /// place (`DW0988`); its eaves stop at a neighbour's wall.
766 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub roof: Option<Roof>,
768}
769
770/// The roof a roofed place carries above its lid (spec-0098 §3).
771///
772/// Both numbers are judgements the plan states against the research record
773/// (`docs/reference/roof-and-facade-craft.md`): a 45° gable over a roof span of
774/// `W` cells rises `⌈(W − 1) / 2⌉` courses, and an eave of one cell is the
775/// idiom. Neither is inferred by the engine.
776#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
777#[serde(deny_unknown_fields)]
778pub struct Roof {
779 /// Courses the roof rises above the ceiling course. `0` is a flat roof whose
780 /// only course is the lid.
781 pub courses: u32,
782 /// Cells the roof zone overhangs the shell footprint on every horizontal
783 /// side, from the ceiling course up. `0` is a roof flush with the walls.
784 pub eaves: u32,
785}
786
787/// Which side of a box a seam sits on.
788///
789/// The engine's existing face vocabulary — the same six names a prefab's face
790/// contract writes (`compiler::faces`), so a reader who knows one knows the
791/// other and no translation table exists to disagree with itself.
792#[derive(
793 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
794)]
795#[serde(rename_all = "kebab-case")]
796pub enum Face {
797 /// `+x`.
798 East,
799 /// `-x`.
800 West,
801 /// `+y`.
802 Up,
803 /// `-y`.
804 Down,
805 /// `+z`.
806 South,
807 /// `-z`.
808 North,
809}
810
811impl Face {
812 /// The unit vector this face points along.
813 #[must_use]
814 pub fn vector(self) -> [i64; 3] {
815 match self {
816 Face::East => [1, 0, 0],
817 Face::West => [-1, 0, 0],
818 Face::Up => [0, 1, 0],
819 Face::Down => [0, -1, 0],
820 Face::South => [0, 0, 1],
821 Face::North => [0, 0, -1],
822 }
823 }
824
825 /// True for a face whose plane is horizontal (a floor or a ceiling).
826 #[must_use]
827 pub fn is_horizontal_plane(self) -> bool {
828 matches!(self, Face::Up | Face::Down)
829 }
830
831 /// The name a refusal prints.
832 #[must_use]
833 pub fn as_str(self) -> &'static str {
834 match self {
835 Face::East => "east",
836 Face::West => "west",
837 Face::Up => "up",
838 Face::Down => "down",
839 Face::South => "south",
840 Face::North => "north",
841 }
842 }
843}
844
845/// A crossing's position on one box's face, from that box's own low corner.
846///
847/// One integer on a wall face (cells along the face's horizontal axis), a pair
848/// through a floor or ceiling (cells along `x` and `z`). Untagged, so the
849/// document writes `"at": 2` or `"at": [2, 3]`; the face decides which shape is
850/// a position and the other is `DW0828`.
851#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
852#[serde(untagged)]
853pub enum Offset {
854 /// Cells along a vertical face's horizontal axis.
855 Along(i64),
856 /// Cells along `x` and `z` on a horizontal face.
857 Plane([i64; 2]),
858}
859
860/// One traversal edge, allocated: an opening on a face the two boxes share.
861///
862/// The seam carries **no rise** and **no sill**. A rise is `floor(b) − floor(a)`, which the plan
863/// has already stated by putting the two places where it put them; a second
864/// declaration of it could only ever agree or be a refusal teaching nothing the
865/// datums did not already say. `DW0830` and `DW0831` judge the derived number,
866/// and the stage-5 observer of the built bytes judges the realized one against
867/// the same derivation rather than against an author's copy of it.
868#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
869#[serde(deny_unknown_fields)]
870pub struct Seam {
871 /// The graph edge this seam allocates.
872 pub edge: EdgeId,
873 /// Which face **of the edge's `a` box** the seam sits on. The `b` box is the
874 /// neighbour across it (`DW0828`).
875 pub face: Face,
876 /// Where the crossing sits on **`a`'s** face: an offset from `a`'s own low
877 /// corner along the face, never a world coordinate (spec-0059 §2).
878 ///
879 /// * on a vertical face (`east`/`west`/`north`/`south`) — one integer, cells
880 /// along the face's horizontal axis (`z` for east/west, `x` for
881 /// north/south). The **sill is not written**: it is
882 /// `max(floor(a), floor(b))`, which the plan has already stated.
883 /// * on a horizontal face (`up`/`down`) — `[dx, dz]`, cells along `x` and
884 /// `z`.
885 ///
886 /// Omitted, the crossing is **centred** on the face: `(extent - width) div
887 /// 2`. An offset that leaves the face, or of the wrong shape for the face,
888 /// is `DW0828`.
889 #[serde(default, skip_serializing_if = "Option::is_none")]
890 pub at: Option<Offset>,
891 /// Where the same crossing sits on **`b`'s** face — the same offset, taken
892 /// from `b`'s own low corner. Omitted, centred on `b`'s face. The packing
893 /// places `b` so that the two agree: `corner(b) = corner(a) + at - meets`
894 /// along the face (spec-0059 §3). On a seam whose two boxes both already
895 /// stand, the two must name the same cells (`DW0828`).
896 #[serde(default, skip_serializing_if = "Option::is_none")]
897 pub meets: Option<Offset>,
898 /// **A PORTAL**: a named opening from the metrics table's standard set
899 /// (`DW0812` on a name the table does not define), or a size the seam
900 /// declares itself, `{"width": w, "height": h}` — the author's own opening,
901 /// a rope bridge's end one cell wide included. `DW0829` confirms either fits
902 /// the shared face. A body crosses at exactly the cells `at` and the opening
903 /// allocate.
904 ///
905 /// **Exactly one of this and [`Seam::contact`]** (`DW0876`).
906 #[serde(default, skip_serializing_if = "Option::is_none")]
907 pub opening: Option<OpeningSpec>,
908 /// **A CONTACT**: the two places simply meet along a front, rather than
909 /// through a doorway (spec-0053 §4).
910 ///
911 /// **Exactly one of this and [`Seam::opening`]** (`DW0876`).
912 ///
913 /// The width of a front where two places meet is a fact of those two boxes'
914 /// shared face — per-campaign geometry, continuous — so it is never a named
915 /// standard. A table that enumerated it would gain a new entry per campaign,
916 /// which is content reproduced in the opening set as a standard: an `opening.gate-front` of 21×4 is content wearing a standard's
917 /// clothes (spec-0053 §7).
918 #[serde(default, skip_serializing_if = "Option::is_none")]
919 pub contact: Option<Contact>,
920 /// Which of the edge's two boxes hosts the stair massing. Required on a
921 /// `stair` edge and refused on any other (`DW0830`, `DW0824`).
922 #[serde(default, skip_serializing_if = "Option::is_none")]
923 pub stair_in: Option<NodeId>,
924 /// **What the crossing is, in a few words** (spec-0098 §2c) — "a wooden
925 /// arch bridge, 3 wide", "a stone stair, down 4". A creative judgement the
926 /// plan states once and `delvec allocation` hands to BOTH places the seam
927 /// joins, so each designs its side of the interface knowing what meets it.
928 /// Never player-facing, so never translated.
929 pub form: String,
930}
931
932/// **A portal's opening**: a named standard, or a size the seam declares.
933#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
934#[serde(untagged)]
935pub enum OpeningSpec {
936 /// A named standard from the metrics table's opening set.
937 Named(String),
938 /// A size the seam declares itself.
939 Declared(DeclaredOpening),
940}
941
942/// An opening the seam sizes itself: cells on the face's own two in-plane
943/// axes — along the wall and up it on a vertical face, `x` and `z` through a
944/// floor.
945#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
946#[serde(deny_unknown_fields)]
947pub struct DeclaredOpening {
948 /// Clear width, in cells.
949 pub width: NonZeroU32,
950 /// Clear height, in cells.
951 pub height: NonZeroU32,
952}
953
954impl OpeningSpec {
955 /// The opening's size: the named standard's, through [`Metrics::resolve`]
956 /// (the one path from a name to an entry), or the declared one.
957 ///
958 /// # Errors
959 ///
960 /// [`crate::metrics::UnknownMetric`] for a name the table does not define,
961 /// which the caller turns into `DW0812`.
962 pub fn resolve(
963 &self,
964 table: &Metrics,
965 reads: &mut Reads,
966 ) -> Result<crate::metrics::Opening, crate::metrics::UnknownMetric> {
967 match self {
968 OpeningSpec::Named(name) => {
969 let entry = table.resolve(MetricKind::Opening, name)?;
970 match entry.value(reads) {
971 MetricValue::Opening(o) => Ok(*o),
972 _ => unreachable!("an opening entry carries an opening"),
973 }
974 }
975 OpeningSpec::Declared(o) => Ok(crate::metrics::Opening {
976 width: o.width.get(),
977 height: o.height.get(),
978 }),
979 }
980 }
981
982 /// How the opening reads in a message: the standard's name, or `WxH`.
983 #[must_use]
984 pub fn describe(&self) -> String {
985 match self {
986 OpeningSpec::Named(name) => format!("`{name}`"),
987 OpeningSpec::Declared(o) => format!("declared {}x{}", o.width, o.height),
988 }
989 }
990}
991
992/// A mass the whole itself owns.
993#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
994#[serde(deny_unknown_fields)]
995pub struct Volume {
996 /// Volume id (`volume/<kebab>`), unique within the plan.
997 pub id: VolumeId,
998 /// The cells it covers.
999 pub region: WorldBox,
1000 /// What the whole is doing with them.
1001 pub role: VolumeRole,
1002 /// What this mass is, for a reader of the plan.
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub note: Option<String>,
1005 /// The block state this volume is made of. Absent, the block of its kind
1006 /// comes from the plan's `fill` (spec-0098 §2b): a `massif` is the
1007 /// `solid` fill's block, or the `open` fill's `below`; a `ground` is the
1008 /// `open` fill's `surface` over `below`, or the `solid` fill's block. A
1009 /// `clearance` is air and names no block (`DW0193` if it does).
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1011 pub block: Option<String>,
1012}
1013
1014/// What a whole-owned volume is for.
1015#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1016#[serde(rename_all = "kebab-case")]
1017pub enum VolumeRole {
1018 /// Solid mass the places are cut into — the mountain around a cave system.
1019 Massif,
1020 /// The ground the places stand on.
1021 Ground,
1022 /// Air the whole keeps empty — the sky a silhouette needs, the drop a
1023 /// vista looks over.
1024 Clearance,
1025}
1026
1027/// One guarded comparison binding the plan to a fact of the written brief.
1028#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1029#[serde(deny_unknown_fields)]
1030pub struct Identity {
1031 /// The brief fact this holds the plan to (`DW0112` if the brief has no such
1032 /// fact).
1033 pub fact: FactId,
1034 /// What is measured off the plan.
1035 pub measure: Measure,
1036 /// How the measurement must stand to the fact's value.
1037 pub cmp: Cmp,
1038}
1039
1040/// What an identity measures off the plan.
1041///
1042/// A small **fixed** vocabulary, spelled as a tagged union rather than as the
1043/// spec's `box(<node>).extent.x` string. The vocabulary is exactly the spec's
1044/// five; what changes is that a measure is parsed by serde instead of by a
1045/// grammar this module would have had to write, own and document — so an
1046/// unknown measure is an ordinary `DW0100`, a node it names is checked like
1047/// every other reference, and the growth the spec's marked judgement predicts is
1048/// a variant rather than a second escaping rule. §9 records the departure.
1049#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1050#[serde(tag = "of", rename_all = "kebab-case", deny_unknown_fields)]
1051pub enum Measure {
1052 /// The whole region's extent on one axis, in blocks.
1053 RegionExtent {
1054 /// Which axis.
1055 axis: Axis,
1056 },
1057 /// One place's footprint on one horizontal axis, in blocks.
1058 BoxExtent {
1059 /// The place.
1060 node: NodeId,
1061 /// Which horizontal axis.
1062 axis: PlanAxis,
1063 },
1064 /// One place's headroom over its walk plane, in blocks. A sky-open place
1065 /// measures the courses of air its ceiling declares.
1066 BoxHeight {
1067 /// The place.
1068 node: NodeId,
1069 },
1070 /// The horizontal distance between two places' footprint centres, in blocks.
1071 /// Euclidean on `x`/`z`; the standoff a brief states between two things.
1072 DistanceXz {
1073 /// One place.
1074 from: NodeId,
1075 /// The other.
1076 to: NodeId,
1077 },
1078 /// A named ground plane's world `y`.
1079 DatumY {
1080 /// The plane.
1081 datum: DatumId,
1082 },
1083}
1084
1085/// One of the three world axes.
1086#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1087#[serde(rename_all = "kebab-case")]
1088pub enum Axis {
1089 /// East–west.
1090 X,
1091 /// Up–down.
1092 Y,
1093 /// North–south.
1094 Z,
1095}
1096
1097/// One of the two horizontal axes.
1098///
1099/// A separate type from [`Axis`] rather than a `y` some check refuses: a box is
1100/// a footprint, so its extent has no `y` to ask about, and an unrepresentable
1101/// state needs no diagnostic to police it.
1102#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1103#[serde(rename_all = "kebab-case")]
1104pub enum PlanAxis {
1105 /// East–west.
1106 X,
1107 /// North–south.
1108 Z,
1109}
1110
1111/// How a measurement must stand to its fact's value.
1112#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1113#[serde(rename_all = "kebab-case")]
1114pub enum Cmp {
1115 /// Exactly.
1116 Eq,
1117 /// Strictly under.
1118 Lt,
1119 /// At most.
1120 Le,
1121 /// Strictly over.
1122 Gt,
1123 /// At least.
1124 Ge,
1125}
1126
1127impl Cmp {
1128 fn holds(self, measured: f64, fact: f64) -> bool {
1129 match self {
1130 Cmp::Eq => (measured - fact).abs() < 1e-9,
1131 Cmp::Lt => measured < fact,
1132 Cmp::Le => measured <= fact,
1133 Cmp::Gt => measured > fact,
1134 Cmp::Ge => measured >= fact,
1135 }
1136 }
1137
1138 fn as_str(self) -> &'static str {
1139 match self {
1140 Cmp::Eq => "exactly",
1141 Cmp::Lt => "under",
1142 Cmp::Le => "at most",
1143 Cmp::Gt => "over",
1144 Cmp::Ge => "at least",
1145 }
1146 }
1147}
1148
1149/// A `vision` edge, embedded: the segment the stage-5 battery walks.
1150///
1151/// A vision edge gets a sightline rather than a seam because a vista's two ends
1152/// are routinely not adjacent — a bell tower seen from a shore shares no face
1153/// with it — so the seam construct cannot state the one thing a vision edge
1154/// asserts. A window between neighbours is simply a short sightline
1155/// (spec-0049 §4.4).
1156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1157#[serde(deny_unknown_fields)]
1158pub struct Sightline {
1159 /// The `vision` edge this embeds.
1160 pub edge: EdgeId,
1161 /// The eye end, in world coordinates — inside the edge's `a` box
1162 /// (`DW0824`).
1163 pub from: [i64; 3],
1164 /// The seen end — inside the edge's `b` box (`DW0824`).
1165 pub to: [i64; 3],
1166}
1167
1168/// A named exterior vantage the walk judges the silhouette from.
1169#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1170#[serde(deny_unknown_fields)]
1171pub struct View {
1172 /// View id (`view/<kebab>`), unique within the plan.
1173 pub id: ViewId,
1174 /// Where the eye stands, in world coordinates.
1175 pub eye: [i64; 3],
1176 /// What it looks at.
1177 pub look_at: [i64; 3],
1178 /// What this view is for.
1179 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub note: Option<String>,
1181}