Skip to main content

weft_lang/
draft_lib.rs

1//! # weft-draft — level design as a verified program
2//!
3//! In weaving, the **draft** is the pattern a cloth is woven from. This is
4//! that, for places: a library of *figures* — halls, colonnades, thresholds,
5//! stairs, groves — that compose into a [plan](infinite_manifest::plan): the
6//! ground, the lights, the ways in and out, and a **bill of requirements**
7//! saying what the place needs without naming a single file.
8//!
9//! Two properties fall out of being Weft, and both matter more than they
10//! sound:
11//!
12//! - **The same brief rebuilds the same place, forever.** No hidden clock, no
13//!   platform float drift; a seed makes siblings on purpose, never by accident.
14//! - **Taste is written down.** Door widths, ceiling ratios, lamp spacing,
15//!   the clearance a veil needs from a spawn — these are constants with names
16//!   ([`infinite_manifest::plan::metric`]), used by every figure, so a layout
17//!   is critiqued by reading it rather than by walking it.
18//!
19//! What this library deliberately cannot do is *shop*. Finding or
20//! commissioning the models a plan asks for is an effect, and effects live at
21//! the edge (`thread level`), never inside a verified program.
22
23use std::collections::{BTreeMap, BTreeSet};
24
25use crate::pack::Package;
26use crate::{hash_def, Def, PrimOp, Term, Ty, WeftHash, FIX_SCALE, FIX_TAU};
27
28// --- the same small comfort layer the modeling library uses ----------------
29
30fn fx(v: f32) -> Term {
31    Term::Fix((v as f64 * FIX_SCALE as f64).round() as i64)
32}
33fn int(v: i64) -> Term {
34    Term::Int(v)
35}
36fn txt(s: &str) -> Term {
37    Term::Text(s.to_string())
38}
39fn var(i: u32) -> Term {
40    Term::Var(i)
41}
42fn p1(op: PrimOp, a: Term) -> Term {
43    Term::Prim(op, vec![a])
44}
45fn p2(op: PrimOp, a: Term, b: Term) -> Term {
46    Term::Prim(op, vec![a, b])
47}
48fn add(a: Term, b: Term) -> Term {
49    p2(PrimOp::FAdd, a, b)
50}
51fn sub(a: Term, b: Term) -> Term {
52    p2(PrimOp::FSub, a, b)
53}
54fn mul(a: Term, b: Term) -> Term {
55    p2(PrimOp::FMul, a, b)
56}
57fn div(a: Term, b: Term) -> Term {
58    p2(PrimOp::FDiv, a, b)
59}
60fn get(t: Term, f: &str) -> Term {
61    Term::Get(Box::new(t), f.to_string())
62}
63fn rec(fields: Vec<(&str, Term)>) -> Term {
64    Term::Rec(
65        fields
66            .into_iter()
67            .map(|(k, v)| (k.to_string(), v))
68            .collect(),
69    )
70}
71fn list(items: Vec<Term>) -> Term {
72    Term::ListNew(items)
73}
74fn rec_ty(fields: Vec<(&str, Ty)>) -> Ty {
75    Ty::Record(
76        fields
77            .into_iter()
78            .map(|(k, v)| (k.to_string(), v))
79            .collect(),
80    )
81}
82fn vec3(x: Term, y: Term, z: Term) -> Term {
83    list(vec![x, y, z])
84}
85fn vec3_ty() -> Ty {
86    Ty::List(Box::new(Ty::Fix))
87}
88/// An empty list of a *known* element type (an empty `ListNew` has none).
89fn none_of(elem: Term) -> Term {
90    Term::Map {
91        cap: 0,
92        list: Box::new(list(vec![elem])),
93        body: Box::new(var(0)),
94    }
95}
96
97const CAP: u32 = 512;
98const REPEAT_CAP: u32 = 128;
99
100// --- the plan ABI, mirroring infinite_manifest::plan ------------------------
101
102fn need_ty() -> Ty {
103    rec_ty(vec![
104        ("at", vec3_ty()),
105        ("commission", Ty::Bool),
106        ("d", Ty::Fix),
107        ("h", Ty::Fix),
108        ("kind", Ty::Text),
109        ("must", Ty::Bool),
110        ("name", Ty::Text),
111        ("solid", Ty::Bool),
112        ("style", Ty::Text),
113        ("tags", Ty::List(Box::new(Ty::Text))),
114        ("tol", Ty::Fix),
115        ("w", Ty::Fix),
116        ("yaw", Ty::Fix),
117    ])
118}
119fn veil_ty() -> Ty {
120    rec_ty(vec![
121        ("at", vec3_ty()),
122        ("label", Ty::Text),
123        ("to", Ty::Text),
124        ("yaw", Ty::Fix),
125    ])
126}
127fn spawn_ty() -> Ty {
128    rec_ty(vec![
129        ("at", vec3_ty()),
130        ("name", Ty::Text),
131        ("yaw", Ty::Fix),
132    ])
133}
134fn light_ty() -> Ty {
135    rec_ty(vec![
136        ("at", vec3_ty()),
137        ("fixture", Ty::Bool),
138        ("intensity", Ty::Fix),
139        ("range", Ty::Fix),
140        ("warm", Ty::Fix),
141    ])
142}
143fn sign_ty() -> Ty {
144    rec_ty(vec![
145        ("at", vec3_ty()),
146        ("h", Ty::Fix),
147        ("text", Ty::Text),
148        ("w", Ty::Fix),
149        ("yaw", Ty::Fix),
150    ])
151}
152fn build_ty() -> Ty {
153    rec_ty(vec![
154        ("at", vec3_ty()),
155        ("d", Ty::Fix),
156        ("h", Ty::Fix),
157        ("material", Ty::Text),
158        ("name", Ty::Text),
159        ("r", Ty::Fix),
160        ("shape", Ty::Text),
161        ("solid", Ty::Bool),
162        ("w", Ty::Fix),
163        ("yaw", Ty::Fix),
164    ])
165}
166fn palette_ty() -> Ty {
167    rec_ty(vec![
168        ("metal", Ty::Text),
169        ("sky", Ty::Text),
170        ("stone", Ty::Text),
171        ("style", Ty::Text),
172        ("wood", Ty::Text),
173    ])
174}
175fn plan_ty() -> Ty {
176    rec_ty(vec![
177        ("description", Ty::Text),
178        ("lights", Ty::List(Box::new(light_ty()))),
179        ("name", Ty::Text),
180        ("needs", Ty::List(Box::new(need_ty()))),
181        ("palette", palette_ty()),
182        ("builds", Ty::List(Box::new(build_ty()))),
183        ("signs", Ty::List(Box::new(sign_ty()))),
184        ("spawns", Ty::List(Box::new(spawn_ty()))),
185        ("veils", Ty::List(Box::new(veil_ty()))),
186    ])
187}
188
189fn need(kind: &str, over: Vec<(&str, Term)>) -> Term {
190    let mut f: BTreeMap<String, Term> = BTreeMap::new();
191    f.insert("kind".into(), txt(kind));
192    f.insert("at".into(), vec3(fx(0.0), fx(0.0), fx(0.0)));
193    f.insert("yaw".into(), fx(0.0));
194    for k in ["w", "h", "d"] {
195        f.insert(k.into(), fx(0.0));
196    }
197    f.insert("tol".into(), fx(0.25));
198    f.insert("style".into(), txt(""));
199    f.insert("tags".into(), none_of(txt("")));
200    f.insert("commission".into(), Term::Bool(true));
201    f.insert("must".into(), Term::Bool(true));
202    f.insert("solid".into(), Term::Bool(true));
203    f.insert("name".into(), txt(""));
204    for (k, v) in over {
205        f.insert(k.into(), v);
206    }
207    Term::Rec(f)
208}
209
210fn copy_need(elem: u32, over: Vec<(&str, Term)>) -> Term {
211    let mut f: BTreeMap<String, Term> = BTreeMap::new();
212    for k in [
213        "at",
214        "commission",
215        "d",
216        "h",
217        "kind",
218        "must",
219        "name",
220        "solid",
221        "style",
222        "tags",
223        "tol",
224        "w",
225        "yaw",
226    ] {
227        f.insert(k.into(), get(var(elem), k));
228    }
229    for (k, v) in over {
230        f.insert(k.into(), v);
231    }
232    Term::Rec(f)
233}
234
235fn def(params: Vec<Ty>, ret: Ty, body: Term) -> Def {
236    Def {
237        params,
238        ret,
239        effects: BTreeSet::new(),
240        body,
241        pre: None,
242        post: None,
243    }
244}
245
246struct Builder {
247    defs: Vec<Def>,
248    by_name: BTreeMap<String, (usize, WeftHash)>,
249}
250
251impl Builder {
252    fn new() -> Self {
253        Builder {
254            defs: Vec::new(),
255            by_name: BTreeMap::new(),
256        }
257    }
258    fn add(&mut self, name: &str, d: Def) -> WeftHash {
259        let h = hash_def(&d);
260        self.defs.push(d);
261        self.by_name
262            .insert(name.to_string(), (self.defs.len() - 1, h));
263        h
264    }
265    fn call(&self, name: &str, args: Vec<Term>) -> Term {
266        Term::Call(self.by_name[name].1, args)
267    }
268    fn idx(&self, name: &str) -> usize {
269        self.by_name[name].0
270    }
271}
272
273/// Build the `weft-draft` package.
274pub fn package() -> Package {
275    let mut b = Builder::new();
276
277    // --- one of a thing, somewhere ----------------------------------------
278    b.add(
279        "need",
280        def(
281            vec![
282                Ty::Text,
283                Ty::Fix,
284                Ty::Fix,
285                Ty::Fix,
286                Ty::Fix,
287                Ty::Fix,
288                Ty::Fix,
289                Ty::Fix,
290            ],
291            Ty::List(Box::new(need_ty())),
292            // need(kind, x, y, z, yaw, w, h, d)
293            list(vec![need(
294                "",
295                vec![
296                    ("kind", var(7)),
297                    ("at", vec3(var(6), var(5), var(4))),
298                    ("yaw", var(3)),
299                    ("w", var(2)),
300                    ("h", var(1)),
301                    ("d", var(0)),
302                ],
303            )]),
304        ),
305    );
306    b.add(
307        "join",
308        def(
309            vec![Ty::List(Box::new(need_ty())), Ty::List(Box::new(need_ty()))],
310            Ty::List(Box::new(need_ty())),
311            p2(PrimOp::ListCat, var(1), var(0)),
312        ),
313    );
314    b.add(
315        "styled",
316        def(
317            vec![Ty::List(Box::new(need_ty())), Ty::Text],
318            Ty::List(Box::new(need_ty())),
319            Term::Map {
320                cap: CAP,
321                list: Box::new(var(1)),
322                body: Box::new(copy_need(0, vec![("style", var(1))])),
323            },
324        ),
325    );
326    b.add(
327        "loose",
328        def(
329            vec![Ty::List(Box::new(need_ty())), Ty::Fix],
330            Ty::List(Box::new(need_ty())),
331            Term::Map {
332                cap: CAP,
333                list: Box::new(var(1)),
334                body: Box::new(copy_need(0, vec![("tol", var(1))])),
335            },
336        ),
337    );
338    b.add(
339        "scatterable",
340        def(
341            vec![Ty::List(Box::new(need_ty()))],
342            Ty::List(Box::new(need_ty())),
343            Term::Map {
344                cap: CAP,
345                list: Box::new(var(0)),
346                body: Box::new(copy_need(
347                    0,
348                    vec![
349                        ("must", Term::Bool(false)),
350                        ("commission", Term::Bool(false)),
351                    ],
352                )),
353            },
354        ),
355    );
356
357    // --- slots: repetition that cannot collide -----------------------------
358    // ring_of(needs, n, radius): n copies around a circle, each turned to
359    // face the centre. Slots, not free coordinates — overlap is impossible
360    // by construction, which removes a whole class of layout fault.
361    b.add(
362        "ring_of",
363        def(
364            vec![Ty::List(Box::new(need_ty())), Ty::Int, Ty::Fix],
365            Ty::List(Box::new(need_ty())),
366            Term::Fold {
367                cap: REPEAT_CAP,
368                list: Box::new(Term::Iota {
369                    cap: REPEAT_CAP,
370                    count: Box::new(var(1)),
371                }),
372                init: Box::new(none_of(need("column", vec![]))),
373                // [i=0, acc=1, radius=2, n=3, needs=4]
374                //
375                // Slots sit HALF A STEP off the axes. A ring that starts at 0°
376                // puts a column dead in front of the entrance and another in
377                // front of the far threshold — the two places a visitor is
378                // guaranteed to be looking. Offsetting keeps both axes clear,
379                // which is why colonnades in real buildings have a gap where
380                // the door is.
381                body: Box::new(Term::Let(
382                    // az in degrees
383                    Box::new(mul(
384                        div(
385                            add(p1(PrimOp::FixOfInt, var(0)), fx(0.5)),
386                            p1(PrimOp::FixOfInt, var(3)),
387                        ),
388                        fx(360.0),
389                    )),
390                    // [az=0, i=1, acc=2, radius=3, n=4, needs=5]
391                    Box::new(p2(
392                        PrimOp::ListCat,
393                        var(2),
394                        Term::Map {
395                            cap: CAP,
396                            list: Box::new(var(5)),
397                            // [elem=0, az=1, i=2, acc=3, radius=4, n=5, needs=6]
398                            body: Box::new(copy_need(
399                                0,
400                                vec![
401                                    (
402                                        "at",
403                                        // The engine's azimuth convention
404                                        // (infinite_manifest::arch): 0 faces
405                                        // −z, away from arrivals; a point at
406                                        // az sits at (sin az, −cos az)·r.
407                                        vec3(
408                                            mul(
409                                                var(4),
410                                                p1(
411                                                    PrimOp::FSin,
412                                                    mul(div(var(1), fx(360.0)), Term::Fix(FIX_TAU)),
413                                                ),
414                                            ),
415                                            // Ground level: models stand on
416                                            // their own base (facts.origin).
417                                            fx(0.0),
418                                            sub(
419                                                fx(0.0),
420                                                mul(
421                                                    var(4),
422                                                    p1(
423                                                        PrimOp::FCos,
424                                                        mul(
425                                                            div(var(1), fx(360.0)),
426                                                            Term::Fix(FIX_TAU),
427                                                        ),
428                                                    ),
429                                                ),
430                                            ),
431                                        ),
432                                    ),
433                                    // Face the centre: ψ = −az, the same law
434                                    // arch::Corridor::yaw_across derives.
435                                    ("yaw", sub(fx(0.0), var(1))),
436                                ],
437                            )),
438                        },
439                    )),
440                )),
441            },
442        ),
443    );
444    // row_of(needs, n, from_x, from_z, dx, dz, yaw)
445    b.add(
446        "row_of",
447        def(
448            vec![
449                Ty::List(Box::new(need_ty())),
450                Ty::Int,
451                Ty::Fix,
452                Ty::Fix,
453                Ty::Fix,
454                Ty::Fix,
455                Ty::Fix,
456            ],
457            Ty::List(Box::new(need_ty())),
458            Term::Fold {
459                cap: REPEAT_CAP,
460                list: Box::new(Term::Iota {
461                    cap: REPEAT_CAP,
462                    count: Box::new(var(5)),
463                }),
464                init: Box::new(none_of(need("column", vec![]))),
465                // [i=0, acc=1, yaw=2, dz=3, dx=4, fz=5, fx=6, n=7, needs=8]
466                body: Box::new(p2(
467                    PrimOp::ListCat,
468                    var(1),
469                    Term::Map {
470                        cap: CAP,
471                        list: Box::new(var(8)),
472                        // [elem=0, i=1, acc=2, yaw=3, dz=4, dx=5, fz=6, fx=7, n=8, needs=9]
473                        body: Box::new(copy_need(
474                            0,
475                            vec![
476                                (
477                                    "at",
478                                    vec3(
479                                        add(var(7), mul(p1(PrimOp::FixOfInt, var(1)), var(5))),
480                                        fx(0.0),
481                                        add(var(6), mul(p1(PrimOp::FixOfInt, var(1)), var(4))),
482                                    ),
483                                ),
484                                ("yaw", var(3)),
485                            ],
486                        )),
487                    },
488                )),
489            },
490        ),
491    );
492
493    // --- built stone: what the browser draws itself ------------------------
494    //
495    // A wall segment is a box. Asking a store for one — or worse,
496    // commissioning it — would be absurd, so built geometry is its own list
497    // and never goes shopping. This is also what keeps a drafted world light:
498    // a hundred wall boxes are one prefab and one instanced draw.
499    let build = |shape: &str, over: Vec<(&str, Term)>| {
500        let mut f: BTreeMap<String, Term> = BTreeMap::new();
501        f.insert("shape".into(), txt(shape));
502        f.insert("at".into(), vec3(fx(0.0), fx(0.0), fx(0.0)));
503        f.insert("yaw".into(), fx(0.0));
504        for k in ["r", "w", "d"] {
505            f.insert(k.into(), fx(0.0));
506        }
507        f.insert("h".into(), fx(0.1));
508        f.insert("material".into(), txt("stone"));
509        f.insert("solid".into(), Term::Bool(true));
510        f.insert("name".into(), txt(""));
511        for (k, v) in over {
512            f.insert(k.into(), v);
513        }
514        Term::Rec(f)
515    };
516    let empty_builds = none_of(build("box", vec![]));
517
518    b.add(
519        "ground",
520        def(
521            vec![Ty::Fix],
522            Ty::List(Box::new(build_ty())),
523            list(vec![build(
524                "disc",
525                vec![
526                    ("r", var(0)),
527                    ("h", fx(0.12)),
528                    ("at", vec3(fx(0.0), fx(0.02), fx(0.0))),
529                    ("material", txt("floor")),
530                    ("name", txt("floor")),
531                ],
532            )]),
533        ),
534    );
535    b.add(
536        "join_builds",
537        def(
538            vec![
539                Ty::List(Box::new(build_ty())),
540                Ty::List(Box::new(build_ty())),
541            ],
542            Ty::List(Box::new(build_ty())),
543            p2(PrimOp::ListCat, var(1), var(0)),
544        ),
545    );
546
547    // labelled(needs, prefix): the same courtesy for requirements. The binder
548    // reports what it bound by name; "column" twelve times over two different
549    // colonnades is a report nobody can read.
550    b.add(
551        "labelled",
552        def(
553            vec![Ty::List(Box::new(need_ty())), Ty::Text],
554            Ty::List(Box::new(need_ty())),
555            Term::Map {
556                cap: CAP,
557                list: Box::new(var(1)),
558                // [want=0, prefix=1, needs=2]
559                body: Box::new(rec(vec![
560                    ("at", get(var(0), "at")),
561                    ("commission", get(var(0), "commission")),
562                    ("d", get(var(0), "d")),
563                    ("h", get(var(0), "h")),
564                    ("kind", get(var(0), "kind")),
565                    ("must", get(var(0), "must")),
566                    ("name", p2(PrimOp::Concat, var(1), get(var(0), "name"))),
567                    ("solid", get(var(0), "solid")),
568                    ("style", get(var(0), "style")),
569                    ("tags", get(var(0), "tags")),
570                    ("tol", get(var(0), "tol")),
571                    ("w", get(var(0), "w")),
572                    ("yaw", get(var(0), "yaw")),
573                ])),
574            },
575        ),
576    );
577
578    // prefixed(builds, prefix): rename a run of built pieces. Two rings in one
579    // plan are both made of "wall" — and a reader (or a linter, or the next
580    // agent) then cannot tell the rotunda from the boundary. Naming is part of
581    // the design, so it gets a combinator rather than a parameter on every
582    // figure that emits geometry.
583    b.add(
584        "prefixed",
585        def(
586            vec![Ty::List(Box::new(build_ty())), Ty::Text],
587            Ty::List(Box::new(build_ty())),
588            Term::Map {
589                cap: CAP,
590                list: Box::new(var(1)),
591                // [piece=0, prefix=1, builds=2]
592                body: Box::new(rec(vec![
593                    ("at", get(var(0), "at")),
594                    ("d", get(var(0), "d")),
595                    ("h", get(var(0), "h")),
596                    ("material", get(var(0), "material")),
597                    ("name", p2(PrimOp::Concat, var(1), get(var(0), "name"))),
598                    ("r", get(var(0), "r")),
599                    ("shape", get(var(0), "shape")),
600                    ("solid", get(var(0), "solid")),
601                    ("w", get(var(0), "w")),
602                    ("yaw", get(var(0), "yaw")),
603                ])),
604            },
605        ),
606    );
607
608    // wall_ring(r, n, h, thick, gate_az, gate_reach): a ring of wall segments
609    // with openings. A gate is an azimuth; segments whose midpoint falls
610    // within `reach` of one are simply not built — which is how a doorway
611    // exists in a wall that was never a hole.
612    // |a − b| folded into 0…180. There is no modulo in Weft, so the fold is
613    // written out — and it must take the absolute value TWICE. A gate at
614    // −125° against a wall at 262° differs by 387°, and `360 − 387` is
615    // negative: without the second abs that reads as "zero degrees apart",
616    // and the gate silently eats half the ring. (It did. The wall count in
617    // the test is what caught it.)
618    let ang_dist = |a: Term, bb: Term| {
619        let abs = |t: Term| {
620            Term::Let(
621                Box::new(t),
622                Box::new(Term::If(
623                    Box::new(p2(PrimOp::FLt, var(0), fx(0.0))),
624                    Box::new(sub(fx(0.0), var(0))),
625                    Box::new(var(0)),
626                )),
627            )
628        };
629        Term::Let(
630            Box::new(abs(sub(a, bb))),
631            Box::new(Term::If(
632                Box::new(p2(PrimOp::FLt, fx(180.0), var(0))),
633                Box::new(abs(sub(fx(360.0), var(0)))),
634                Box::new(var(0)),
635            )),
636        )
637    };
638    b.add(
639        "wall_ring",
640        def(
641            vec![
642                Ty::Fix,
643                Ty::Int,
644                Ty::Fix,
645                Ty::Fix,
646                Ty::List(Box::new(Ty::Fix)),
647                Ty::Fix,
648            ],
649            Ty::List(Box::new(build_ty())),
650            Term::Fold {
651                cap: REPEAT_CAP,
652                list: Box::new(Term::Iota {
653                    cap: REPEAT_CAP,
654                    count: Box::new(var(4)),
655                }),
656                init: Box::new(empty_builds.clone()),
657                // [i=0, acc=1, reach=2, gates=3, thick=4, h=5, n=6, r=7]
658                body: Box::new(Term::Let(
659                    // az of this segment's midpoint
660                    Box::new(mul(
661                        div(
662                            add(p1(PrimOp::FixOfInt, var(0)), fx(0.5)),
663                            p1(PrimOp::FixOfInt, var(6)),
664                        ),
665                        fx(360.0),
666                    )),
667                    // [az=0, i=1, acc=2, reach=3, gates=4, thick=5, h=6, n=7, r=8]
668                    Box::new(Term::Let(
669                        // is this segment inside a gate?
670                        Box::new(Term::Fold {
671                            cap: 16,
672                            list: Box::new(var(4)),
673                            init: Box::new(Term::Bool(false)),
674                            // [gate=0, open=1, az=2, i=3, acc=4, reach=5, …]
675                            body: Box::new(p2(
676                                PrimOp::Or,
677                                var(1),
678                                p2(PrimOp::FLe, ang_dist(var(2), var(0)), var(5)),
679                            )),
680                        }),
681                        // [gated=0, az=1, i=2, acc=3, reach=4, gates=5, thick=6, h=7, n=8, r=9]
682                        Box::new(Term::If(
683                            Box::new(var(0)),
684                            Box::new(var(3)),
685                            Box::new(p2(
686                                PrimOp::ListCat,
687                                var(3),
688                                list(vec![
689                                    build(
690                                        "box",
691                                        vec![
692                                            (
693                                                "at",
694                                                vec3(
695                                                    mul(
696                                                        var(9),
697                                                        p1(
698                                                            PrimOp::FSin,
699                                                            mul(
700                                                                div(var(1), fx(360.0)),
701                                                                Term::Fix(FIX_TAU),
702                                                            ),
703                                                        ),
704                                                    ),
705                                                    div(var(7), fx(2.0)),
706                                                    sub(
707                                                        fx(0.0),
708                                                        mul(
709                                                            var(9),
710                                                            p1(
711                                                                PrimOp::FCos,
712                                                                mul(
713                                                                    div(var(1), fx(360.0)),
714                                                                    Term::Fix(FIX_TAU),
715                                                                ),
716                                                            ),
717                                                        ),
718                                                    ),
719                                                ),
720                                            ),
721                                            // tangent to the ring: ψ = −az
722                                            ("yaw", sub(fx(0.0), var(1))),
723                                            // chord + a little overlap so the ring reads sealed
724                                            (
725                                                "w",
726                                                add(
727                                                    mul(
728                                                        mul(fx(2.0), var(9)),
729                                                        p1(
730                                                            PrimOp::FSin,
731                                                            div(
732                                                                Term::Fix(FIX_TAU / 2),
733                                                                p1(PrimOp::FixOfInt, var(8)),
734                                                            ),
735                                                        ),
736                                                    ),
737                                                    fx(0.2),
738                                                ),
739                                            ),
740                                            ("h", var(7)),
741                                            ("d", var(6)),
742                                            ("material", txt("stone")),
743                                            ("name", txt("wall")),
744                                        ],
745                                    ),
746                                    // The coping: a finished top course. Without
747                                    // it a wall reads as a slab someone stood up.
748                                    build(
749                                        "box",
750                                        vec![
751                                            (
752                                                "at",
753                                                vec3(
754                                                    mul(
755                                                        var(9),
756                                                        p1(
757                                                            PrimOp::FSin,
758                                                            mul(
759                                                                div(var(1), fx(360.0)),
760                                                                Term::Fix(FIX_TAU),
761                                                            ),
762                                                        ),
763                                                    ),
764                                                    add(var(7), fx(0.09)),
765                                                    sub(
766                                                        fx(0.0),
767                                                        mul(
768                                                            var(9),
769                                                            p1(
770                                                                PrimOp::FCos,
771                                                                mul(
772                                                                    div(var(1), fx(360.0)),
773                                                                    Term::Fix(FIX_TAU),
774                                                                ),
775                                                            ),
776                                                        ),
777                                                    ),
778                                                ),
779                                            ),
780                                            ("yaw", sub(fx(0.0), var(1))),
781                                            (
782                                                "w",
783                                                add(
784                                                    mul(
785                                                        mul(fx(2.0), var(9)),
786                                                        p1(
787                                                            PrimOp::FSin,
788                                                            div(
789                                                                Term::Fix(FIX_TAU / 2),
790                                                                p1(PrimOp::FixOfInt, var(8)),
791                                                            ),
792                                                        ),
793                                                    ),
794                                                    fx(0.3),
795                                                ),
796                                            ),
797                                            ("h", fx(0.18)),
798                                            ("d", add(var(6), fx(0.12))),
799                                            ("solid", Term::Bool(false)),
800                                            ("material", txt("stone")),
801                                            ("name", txt("coping")),
802                                        ],
803                                    ),
804                                ]),
805                            )),
806                        )),
807                    )),
808                )),
809            },
810        ),
811    );
812
813    // gated(needs, gates, reach): drop the ones standing in a doorway. A ring
814    // of columns and a ring of walls have to agree about where the gates are,
815    // or the wall opens politely and a column stands in the gap. (It did. You
816    // walk up to a gallery entrance and there is a column in it.) A need's
817    // azimuth is recoverable from its facing — a ring faces inward, ψ = −az —
818    // so this needs no new information, only the same arithmetic the wall did.
819    b.add(
820        "gated",
821        def(
822            vec![
823                Ty::List(Box::new(need_ty())),
824                Ty::List(Box::new(Ty::Fix)),
825                Ty::Fix,
826            ],
827            Ty::List(Box::new(need_ty())),
828            Term::Fold {
829                cap: CAP,
830                list: Box::new(var(2)),
831                init: Box::new(none_of(need("column", vec![]))),
832                // [want=0, acc=1, reach=2, gates=3, needs=4]
833                body: Box::new(Term::Let(
834                    Box::new(sub(fx(0.0), get(var(0), "yaw"))),
835                    // [az=0, want=1, acc=2, reach=3, gates=4, needs=5]
836                    Box::new(Term::Let(
837                        Box::new(Term::Fold {
838                            cap: 16,
839                            list: Box::new(var(4)),
840                            init: Box::new(Term::Bool(false)),
841                            // [gate=0, blocked=1, az=2, want=3, acc=4, reach=5, …]
842                            body: Box::new(p2(
843                                PrimOp::Or,
844                                var(1),
845                                p2(PrimOp::FLe, ang_dist(var(2), var(0)), var(5)),
846                            )),
847                        }),
848                        // [blocked=0, az=1, want=2, acc=3, reach=4, …]
849                        Box::new(Term::If(
850                            Box::new(var(0)),
851                            Box::new(var(3)),
852                            Box::new(p2(PrimOp::ListCat, var(3), list(vec![var(2)]))),
853                        )),
854                    )),
855                )),
856            },
857        ),
858    );
859
860    // enclosure(r, n, h, gate_az, reach): the grounds and the boundary wall.
861    // A great building needs somewhere to *be* — the outer ring is what makes
862    // the rotunda read as standing in a precinct rather than floating on a
863    // disc, and it is the difference between a model and a place.
864    b.add(
865        "enclosure",
866        def(
867            vec![Ty::Fix, Ty::Int, Ty::Fix, Ty::Fix, Ty::Fix],
868            Ty::List(Box::new(build_ty())),
869            // [reach=0, gate_az=1, h=2, n=3, r=4]
870            p2(
871                PrimOp::ListCat,
872                list(vec![build(
873                    "disc",
874                    vec![
875                        // past the wall, so the precinct has an outside
876                        ("r", mul(var(4), fx(1.18))),
877                        ("h", fx(0.1)),
878                        // resting exactly, not bedded: half its own thickness
879                        // up, so conformance C7 has nothing to say about it
880                        ("at", vec3(fx(0.0), fx(0.05), fx(0.0))),
881                        ("material", txt("ground")),
882                        ("name", txt("grounds")),
883                    ],
884                )]),
885                b.call(
886                    "prefixed",
887                    vec![
888                        b.call(
889                            "wall_ring",
890                            vec![var(4), var(3), var(2), fx(0.45), list(vec![var(1)]), var(0)],
891                        ),
892                        txt("outer "),
893                    ],
894                ),
895            ),
896        ),
897    );
898
899    // --- figures: the shapes places actually come in -----------------------
900    // colonnade(n, radius, height): a ring of columns, sized to the ring so
901    // the proportion holds at any scale.
902    b.add(
903        "colonnade",
904        def(
905            vec![Ty::Int, Ty::Fix, Ty::Fix],
906            Ty::List(Box::new(need_ty())),
907            b.call(
908                "ring_of",
909                vec![
910                    list(vec![need(
911                        "column",
912                        vec![
913                            ("h", var(0)),
914                            ("w", mul(var(0), fx(0.17))),
915                            ("d", mul(var(0), fx(0.17))),
916                            ("tol", mul(var(0), fx(0.08))),
917                            ("tags", list(vec![txt("load-bearing")])),
918                            ("name", txt("column")),
919                        ],
920                    )]),
921                    var(2),
922                    var(1),
923                ],
924            ),
925        ),
926    );
927    // jambs(gates, radius, height, spread): a pair of columns framing every
928    // opening. Gating the ring keeps doorways walkable but leaves them
929    // *unmarked* — a 45° hole in a colonnade reads as a missing piece rather
930    // than an entrance. Two columns on the jambs turn the hole back into a
931    // door, and they land where the ring's own rhythm was interrupted.
932    {
933        // No `Let` for the azimuth, deliberately: binding it would shift every
934        // index in the `r` and `h` terms the caller passed in, and a silent
935        // off-by-one there gave the first version 14-metre columns (it had
936        // grabbed the *spread* argument). Three copies of a cheap term beat
937        // one clever binder.
938        let column_at = |az: Term, r: Term, h: Term| {
939            let rad = |a: Term| mul(div(a, fx(360.0)), Term::Fix(FIX_TAU));
940            need(
941                "column",
942                vec![
943                    (
944                        "at",
945                        vec3(
946                            mul(r.clone(), p1(PrimOp::FSin, rad(az.clone()))),
947                            fx(0.0),
948                            sub(fx(0.0), mul(r, p1(PrimOp::FCos, rad(az.clone())))),
949                        ),
950                    ),
951                    ("yaw", sub(fx(0.0), az)),
952                    ("h", h.clone()),
953                    ("w", mul(h.clone(), fx(0.17))),
954                    ("d", mul(h.clone(), fx(0.17))),
955                    ("tol", mul(h, fx(0.08))),
956                    ("tags", list(vec![txt("load-bearing")])),
957                    ("name", txt("column")),
958                ],
959            )
960        };
961        b.add(
962            "jambs",
963            def(
964                vec![Ty::List(Box::new(Ty::Fix)), Ty::Fix, Ty::Fix, Ty::Fix],
965                Ty::List(Box::new(need_ty())),
966                Term::Fold {
967                    cap: 32,
968                    list: Box::new(var(3)),
969                    init: Box::new(none_of(need("column", vec![]))),
970                    // [gate=0, acc=1, spread=2, height=3, radius=4, gates=5]
971                    body: Box::new(p2(
972                        PrimOp::ListCat,
973                        var(1),
974                        list(vec![
975                            column_at(add(var(0), var(2)), var(4), var(3)),
976                            column_at(sub(var(0), var(2)), var(4), var(3)),
977                        ]),
978                    )),
979                },
980            ),
981        );
982    }
983
984    // lamp_ring(n, radius): light as part of the plan, not an afterthought.
985    b.add(
986        "lamp_ring",
987        def(
988            vec![Ty::Int, Ty::Fix],
989            Ty::List(Box::new(light_ty())),
990            Term::Fold {
991                cap: REPEAT_CAP,
992                list: Box::new(Term::Iota {
993                    cap: REPEAT_CAP,
994                    count: Box::new(var(1)),
995                }),
996                init: Box::new(none_of(rec(vec![
997                    ("at", vec3(fx(0.0), fx(0.0), fx(0.0))),
998                    ("fixture", Term::Bool(true)),
999                    ("intensity", fx(1.0)),
1000                    ("range", fx(9.0)),
1001                    ("warm", fx(1.0)),
1002                ]))),
1003                // [i=0, acc=1, radius=2, n=3]
1004                // Half a step off the axes: a fixture standing in the
1005                // entrance sightline is the first thing a visitor sees, and
1006                // it should never be a lamp post. Offsetting by ½ a step puts
1007                // the lights between the cardinal directions, where they lift
1008                // the room instead of blocking it.
1009                body: Box::new(Term::Let(
1010                    Box::new(mul(
1011                        div(
1012                            add(p1(PrimOp::FixOfInt, var(0)), fx(0.5)),
1013                            p1(PrimOp::FixOfInt, var(3)),
1014                        ),
1015                        Term::Fix(FIX_TAU),
1016                    )),
1017                    // [theta=0, i=1, acc=2, radius=3, n=4]
1018                    Box::new(p2(
1019                        PrimOp::ListCat,
1020                        var(2),
1021                        list(vec![rec(vec![
1022                            (
1023                                "at",
1024                                vec3(
1025                                    mul(var(3), p1(PrimOp::FSin, var(0))),
1026                                    fx(2.4),
1027                                    sub(fx(0.0), mul(var(3), p1(PrimOp::FCos, var(0)))),
1028                                ),
1029                            ),
1030                            ("fixture", Term::Bool(true)),
1031                            ("intensity", fx(1.1)),
1032                            ("range", fx(LAMP_RANGE)),
1033                            ("warm", fx(1.0)),
1034                        ])]),
1035                    )),
1036                )),
1037            },
1038        ),
1039    );
1040
1041    // --- the whole place ---------------------------------------------------
1042    // hall(name, radius, height, columns, style, stone, sky):
1043    // a round hall — floor, colonnade, lamps, a threshold you arrive at and
1044    // a veil opposite it, with the spawn kept clear of the doorway.
1045    b.add(
1046        "hall",
1047        def(
1048            vec![
1049                Ty::Text,
1050                Ty::Fix,
1051                Ty::Fix,
1052                Ty::Int,
1053                Ty::Text,
1054                Ty::Text,
1055                Ty::Text,
1056            ],
1057            plan_ty(),
1058            rec(vec![
1059                ("name", var(6)),
1060                ("description", txt("")),
1061                (
1062                    "palette",
1063                    rec(vec![
1064                        ("style", var(2)),
1065                        ("stone", var(1)),
1066                        ("wood", txt("wood")),
1067                        ("metal", txt("iron")),
1068                        ("sky", var(0)),
1069                    ]),
1070                ),
1071                ("builds", b.call("ground", vec![var(5)])),
1072                (
1073                    "needs",
1074                    b.call(
1075                        "join",
1076                        vec![
1077                            b.call(
1078                                "styled",
1079                                vec![
1080                                    b.call(
1081                                        "colonnade",
1082                                        vec![var(3), mul(var(5), fx(0.82)), var(4)],
1083                                    ),
1084                                    var(2),
1085                                ],
1086                            ),
1087                            // A threshold arch at the far side, framing the veil.
1088                            list(vec![need(
1089                                "arch",
1090                                vec![
1091                                    ("at", vec3(fx(0.0), fx(0.0), mul(var(5), fx(-0.92)))),
1092                                    ("yaw", fx(0.0)),
1093                                    ("w", fx(3.0)),
1094                                    ("h", mul(var(4), fx(0.8))),
1095                                    ("d", fx(0.6)),
1096                                    ("tol", fx(0.5)),
1097                                    ("style", var(2)),
1098                                    ("tags", list(vec![txt("threshold")])),
1099                                    ("name", txt("threshold")),
1100                                ],
1101                            )]),
1102                        ],
1103                    ),
1104                ),
1105                (
1106                    "veils",
1107                    list(vec![rec(vec![
1108                        ("at", vec3(fx(0.0), fx(1.4), mul(var(5), fx(-0.9)))),
1109                        ("yaw", fx(0.0)),
1110                        ("to", txt("")),
1111                        ("label", txt("Onward")),
1112                    ])]),
1113                ),
1114                (
1115                    "spawns",
1116                    list(vec![rec(vec![
1117                        ("at", vec3(fx(0.0), fx(0.0), mul(var(5), fx(0.72)))),
1118                        ("yaw", fx(180.0)),
1119                        ("name", txt("entry")),
1120                    ])]),
1121                ),
1122                (
1123                    "lights",
1124                    b.call("lamp_ring", vec![int(4), mul(var(5), fx(0.55))]),
1125                ),
1126                (
1127                    "signs",
1128                    none_of(rec(vec![
1129                        ("at", vec3(fx(0.0), fx(0.0), fx(0.0))),
1130                        ("h", fx(1.5)),
1131                        ("text", txt("")),
1132                        ("w", fx(2.0)),
1133                        ("yaw", fx(0.0)),
1134                    ])),
1135                ),
1136            ]),
1137        ),
1138    );
1139
1140    // courtyard(name, size, style, stone, sky): a square yard — paved ground,
1141    // a colonnade down two sides, a well at the focus, scattered planting.
1142    b.add(
1143        "courtyard",
1144        def(
1145            vec![Ty::Text, Ty::Fix, Ty::Text, Ty::Text, Ty::Text],
1146            plan_ty(),
1147            rec(vec![
1148                ("name", var(4)),
1149                ("description", txt("")),
1150                (
1151                    "palette",
1152                    rec(vec![
1153                        ("style", var(2)),
1154                        ("stone", var(1)),
1155                        ("wood", txt("wood")),
1156                        ("metal", txt("iron")),
1157                        ("sky", var(0)),
1158                    ]),
1159                ),
1160                (
1161                    "builds",
1162                    list(vec![rec(vec![
1163                        ("shape", txt("slab")),
1164                        ("at", vec3(fx(0.0), fx(0.02), fx(0.0))),
1165                        ("yaw", fx(0.0)),
1166                        ("r", fx(0.0)),
1167                        ("w", var(3)),
1168                        ("d", var(3)),
1169                        ("h", fx(0.12)),
1170                        ("material", txt("stone")),
1171                        ("solid", Term::Bool(true)),
1172                        ("name", txt("paving")),
1173                    ])]),
1174                ),
1175                (
1176                    "needs",
1177                    b.call(
1178                        "join",
1179                        vec![
1180                            b.call(
1181                                "join",
1182                                vec![
1183                                    // two facing colonnades
1184                                    b.call(
1185                                        "row_of",
1186                                        vec![
1187                                            list(vec![need(
1188                                                "column",
1189                                                vec![
1190                                                    ("h", fx(4.2)),
1191                                                    ("w", fx(0.7)),
1192                                                    ("d", fx(0.7)),
1193                                                    ("tol", fx(0.4)),
1194                                                    ("style", var(2)),
1195                                                    ("name", txt("column")),
1196                                                ],
1197                                            )]),
1198                                            int(5),
1199                                            mul(var(3), fx(-0.36)),
1200                                            mul(var(3), fx(-0.36)),
1201                                            fx(0.0),
1202                                            mul(var(3), fx(0.18)),
1203                                            fx(90.0),
1204                                        ],
1205                                    ),
1206                                    b.call(
1207                                        "row_of",
1208                                        vec![
1209                                            list(vec![need(
1210                                                "column",
1211                                                vec![
1212                                                    ("h", fx(4.2)),
1213                                                    ("w", fx(0.7)),
1214                                                    ("d", fx(0.7)),
1215                                                    ("tol", fx(0.4)),
1216                                                    ("style", var(2)),
1217                                                    ("name", txt("column")),
1218                                                ],
1219                                            )]),
1220                                            int(5),
1221                                            mul(var(3), fx(0.36)),
1222                                            mul(var(3), fx(-0.36)),
1223                                            fx(0.0),
1224                                            mul(var(3), fx(0.18)),
1225                                            fx(270.0),
1226                                        ],
1227                                    ),
1228                                ],
1229                            ),
1230                            // the focus: something to walk toward
1231                            list(vec![need(
1232                                "vessel",
1233                                vec![
1234                                    ("at", vec3(fx(0.0), fx(0.0), fx(0.0))),
1235                                    ("w", fx(1.2)),
1236                                    ("h", fx(1.2)),
1237                                    ("d", fx(1.2)),
1238                                    ("tol", fx(0.6)),
1239                                    ("style", var(2)),
1240                                    ("name", txt("focus")),
1241                                ],
1242                            )]),
1243                        ],
1244                    ),
1245                ),
1246                (
1247                    "veils",
1248                    list(vec![rec(vec![
1249                        ("at", vec3(fx(0.0), fx(1.4), mul(var(3), fx(-0.46)))),
1250                        ("yaw", fx(0.0)),
1251                        ("to", txt("")),
1252                        ("label", txt("Onward")),
1253                    ])]),
1254                ),
1255                (
1256                    "spawns",
1257                    list(vec![rec(vec![
1258                        ("at", vec3(fx(0.0), fx(0.0), mul(var(3), fx(0.38)))),
1259                        ("yaw", fx(180.0)),
1260                        ("name", txt("entry")),
1261                    ])]),
1262                ),
1263                (
1264                    "lights",
1265                    b.call("lamp_ring", vec![int(4), mul(var(3), fx(0.3))]),
1266                ),
1267                (
1268                    "signs",
1269                    none_of(rec(vec![
1270                        ("at", vec3(fx(0.0), fx(0.0), fx(0.0))),
1271                        ("h", fx(1.5)),
1272                        ("text", txt("")),
1273                        ("w", fx(2.0)),
1274                        ("yaw", fx(0.0)),
1275                    ])),
1276                ),
1277            ]),
1278        ),
1279    );
1280
1281    // wing(az, from_r, len, width, height): a gallery corridor radiating from
1282    // a rotunda — floor, two side walls, an end wall, and a lintel over the
1283    // gate. Every position and every turn comes from the corridor frame
1284    // (`arch::Corridor`'s law, in Weft): along = 90 − az, across = −az. Wings
1285    // were the museum's hardest geometry to get right by hand; here it is
1286    // written once.
1287    b.add(
1288        "wing",
1289        def(
1290            vec![Ty::Fix, Ty::Fix, Ty::Fix, Ty::Fix, Ty::Fix],
1291            Ty::List(Box::new(build_ty())),
1292            // [h=0, width=1, len=2, from_r=3, az=4]
1293            Term::Let(
1294                // sin az
1295                Box::new(p1(
1296                    PrimOp::FSin,
1297                    mul(div(var(4), fx(360.0)), Term::Fix(FIX_TAU)),
1298                )),
1299                // [s=0, h=1, width=2, len=3, from_r=4, az=5]
1300                Box::new(Term::Let(
1301                    // cos az
1302                    Box::new(p1(
1303                        PrimOp::FCos,
1304                        mul(div(var(5), fx(360.0)), Term::Fix(FIX_TAU)),
1305                    )),
1306                    // [c=0, s=1, h=2, width=3, len=4, from_r=5, az=6]
1307                    Box::new(Term::Let(
1308                        // mid: the corridor's centre, from_r + len/2 out
1309                        Box::new(add(var(5), div(var(4), fx(2.0)))),
1310                        // [mid=0, c=1, s=2, h=3, width=4, len=5, from_r=6, az=7]
1311                        Box::new(list(vec![
1312                            // floor: X-long down the corridor
1313                            build(
1314                                "box",
1315                                vec![
1316                                    (
1317                                        "at",
1318                                        vec3(
1319                                            mul(var(0), var(2)),
1320                                            fx(0.03),
1321                                            sub(fx(0.0), mul(var(0), var(1))),
1322                                        ),
1323                                    ),
1324                                    ("yaw", sub(fx(90.0), var(7))),
1325                                    ("w", var(5)),
1326                                    ("h", fx(0.12)),
1327                                    ("d", var(4)),
1328                                    ("material", txt("floor")),
1329                                    ("name", txt("wing floor")),
1330                                ],
1331                            ),
1332                            // side walls, offset across the corridor
1333                            build(
1334                                "box",
1335                                vec![
1336                                    (
1337                                        "at",
1338                                        vec3(
1339                                            add(
1340                                                mul(var(0), var(2)),
1341                                                mul(div(var(4), fx(2.0)), var(1)),
1342                                            ),
1343                                            div(var(3), fx(2.0)),
1344                                            add(
1345                                                sub(fx(0.0), mul(var(0), var(1))),
1346                                                mul(div(var(4), fx(2.0)), var(2)),
1347                                            ),
1348                                        ),
1349                                    ),
1350                                    ("yaw", sub(fx(90.0), var(7))),
1351                                    ("w", var(5)),
1352                                    ("h", var(3)),
1353                                    ("d", fx(0.4)),
1354                                    ("material", txt("stone")),
1355                                    ("name", txt("wing wall")),
1356                                ],
1357                            ),
1358                            build(
1359                                "box",
1360                                vec![
1361                                    (
1362                                        "at",
1363                                        vec3(
1364                                            sub(
1365                                                mul(var(0), var(2)),
1366                                                mul(div(var(4), fx(2.0)), var(1)),
1367                                            ),
1368                                            div(var(3), fx(2.0)),
1369                                            sub(
1370                                                sub(fx(0.0), mul(var(0), var(1))),
1371                                                mul(div(var(4), fx(2.0)), var(2)),
1372                                            ),
1373                                        ),
1374                                    ),
1375                                    ("yaw", sub(fx(90.0), var(7))),
1376                                    ("w", var(5)),
1377                                    ("h", var(3)),
1378                                    ("d", fx(0.4)),
1379                                    ("name", txt("wing wall")),
1380                                ],
1381                            ),
1382                            // end wall, closing the far end
1383                            build(
1384                                "box",
1385                                vec![
1386                                    (
1387                                        "at",
1388                                        vec3(
1389                                            mul(add(var(6), var(5)), var(2)),
1390                                            div(var(3), fx(2.0)),
1391                                            sub(fx(0.0), mul(add(var(6), var(5)), var(1))),
1392                                        ),
1393                                    ),
1394                                    ("yaw", sub(fx(0.0), var(7))),
1395                                    ("w", add(var(4), fx(0.8))),
1396                                    ("h", var(3)),
1397                                    ("d", fx(0.4)),
1398                                    ("material", txt("stone")),
1399                                    ("name", txt("wing end")),
1400                                ],
1401                            ),
1402                            // lintel over the gate, spanning the opening
1403                            build(
1404                                "box",
1405                                vec![
1406                                    (
1407                                        "at",
1408                                        vec3(
1409                                            mul(add(var(6), fx(0.3)), var(2)),
1410                                            add(var(3), fx(0.15)),
1411                                            sub(fx(0.0), mul(add(var(6), fx(0.3)), var(1))),
1412                                        ),
1413                                    ),
1414                                    ("yaw", sub(fx(0.0), var(7))),
1415                                    ("w", add(var(4), fx(1.2))),
1416                                    ("h", fx(0.32)),
1417                                    ("d", fx(0.5)),
1418                                    ("material", txt("accent")),
1419                                    ("solid", Term::Bool(false)),
1420                                    ("name", txt("lintel")),
1421                                ],
1422                            ),
1423                        ])),
1424                    )),
1425                )),
1426            ),
1427        ),
1428    );
1429
1430    // wing_az(i): where the i-th wing points. Wings alternate either side of
1431    // the entrance and step outward — +55, −55, +125, −125 … — so a hall of
1432    // two wings and a hall of six are the same design at different sizes,
1433    // and neither ever puts a gallery where the door is.
1434    b.add(
1435        "wing_az",
1436        def(
1437            vec![Ty::Int],
1438            Ty::Fix,
1439            Term::Let(
1440                // pair index: how far out from the entrance axis
1441                Box::new(p2(PrimOp::Div, var(0), int(2))),
1442                // [pair=0, i=1]
1443                Box::new(Term::Let(
1444                    Box::new(add(fx(55.0), mul(p1(PrimOp::FixOfInt, var(0)), fx(70.0)))),
1445                    // [mag=0, pair=1, i=2]
1446                    Box::new(Term::If(
1447                        // even index → the +side, odd → the −side
1448                        Box::new(p2(
1449                            PrimOp::EqInt,
1450                            p2(PrimOp::Sub, var(2), p2(PrimOp::Mul, var(1), int(2))),
1451                            int(0),
1452                        )),
1453                        Box::new(var(0)),
1454                        Box::new(sub(fx(0.0), var(0))),
1455                    )),
1456                )),
1457            ),
1458        ),
1459    );
1460
1461    // museum(name, radius, height, columns, wing_len, titles, style, stone, sky):
1462    //
1463    // The encyclopedia's own figure. A walled, colonnaded rotunda with a
1464    // pedestal at its focus, and **one gallery wing per title the brief
1465    // supplies** — each with its name over the gate, a reading board inside,
1466    // and a veil at the far end. The architecture follows the content: two
1467    // sections make a small hall, six make a great one, and neither is a
1468    // different program.
1469    // The wall's gates: the entrance, plus one mouth per wing — derived from
1470    // the titles, so a two-section museum is not a six-gate wall with four
1471    // gaps opening onto nothing.
1472    let gate_azimuths = |titles: Term| -> Term {
1473        p2(
1474            PrimOp::ListCat,
1475            list(vec![fx(180.0)]),
1476            Term::Fold {
1477                cap: 8,
1478                list: Box::new(Term::Iota {
1479                    cap: 8,
1480                    count: Box::new(p1(PrimOp::Len, titles)),
1481                }),
1482                init: Box::new(none_of(fx(0.0))),
1483                // [i=0, acc=1, …]
1484                body: Box::new(p2(
1485                    PrimOp::ListCat,
1486                    var(1),
1487                    list(vec![b.call("wing_az", vec![var(0)])]),
1488                )),
1489            },
1490        )
1491    };
1492    // A wing's contribution, folded per title: the corridor, its named gate,
1493    // its reading board, and the veil that ends it.
1494    b.add(
1495        "museum",
1496        def(
1497            vec![
1498                Ty::Text,
1499                Ty::Fix,
1500                Ty::Fix,
1501                Ty::Int,
1502                Ty::Fix,
1503                Ty::List(Box::new(Ty::Text)),
1504                Ty::Text,
1505                Ty::Text,
1506                Ty::Text,
1507            ],
1508            plan_ty(),
1509            // [sky=0, stone=1, style=2, titles=3, wing_len=4, columns=5,
1510            //  height=6, radius=7, name=8]
1511            rec(vec![
1512                ("name", var(8)),
1513                ("description", txt("")),
1514                (
1515                    "palette",
1516                    rec(vec![
1517                        ("style", var(2)),
1518                        ("stone", var(1)),
1519                        ("wood", txt("wood")),
1520                        ("metal", txt("iron")),
1521                        ("sky", var(0)),
1522                    ]),
1523                ),
1524                ("builds", {
1525                    // floor + gated wall ring, then a corridor per wing.
1526                    let base = b.call(
1527                        "join_builds",
1528                        vec![
1529                            b.call(
1530                                "join_builds",
1531                                vec![
1532                                    // the precinct: grounds and a boundary
1533                                    // wall, open on the entrance axis
1534                                    b.call(
1535                                        "enclosure",
1536                                        vec![
1537                                            // clear of the wings, with
1538                                            // room to walk round them
1539                                            add(add(var(7), var(4)), fx(8.0)),
1540                                            int(32),
1541                                            fx(2.2),
1542                                            fx(180.0),
1543                                            // A ring's segments are offset by
1544                                            // half a step, so a gate on an axis
1545                                            // falls BETWEEN two of them: the
1546                                            // reach has to clear that half-step
1547                                            // (5.6° at 32 segments) or the wall
1548                                            // closes over the way in. It did —
1549                                            // the precinct had no door, and you
1550                                            // only learn that by walking up to
1551                                            // it.
1552                                            fx(6.5),
1553                                        ],
1554                                    ),
1555                                    b.call("ground", vec![var(7)]),
1556                                ],
1557                            ),
1558                            b.call(
1559                                "wall_ring",
1560                                vec![
1561                                    var(7),
1562                                    var(5),
1563                                    // The wall comes up to about 70 % of
1564                                    // the order. Level with the columns it
1565                                    // hides them, and the room loses the
1566                                    // rhythm that makes it a room.
1567                                    mul(var(6), fx(0.7)),
1568                                    fx(0.5),
1569                                    // 13° of reach opens two 15° segments
1570                                    // per gate — a 30° mouth, which at
1571                                    // r = 16 is the 6.8 m a wing is wide.
1572                                    gate_azimuths(var(3)),
1573                                    fx(13.0),
1574                                ],
1575                            ),
1576                        ],
1577                    );
1578                    let with_plinth = b.call(
1579                        "join_builds",
1580                        vec![
1581                            base,
1582                            list(vec![build(
1583                                "cylinder",
1584                                vec![
1585                                    ("at", vec3(fx(0.0), fx(0.3), fx(0.0))),
1586                                    ("r", fx(1.6)),
1587                                    ("h", fx(0.6)),
1588                                    ("material", txt("floor")),
1589                                    ("name", txt("pedestal")),
1590                                ],
1591                            )]),
1592                        ],
1593                    );
1594                    // the walk in, so arrivals land on stone
1595                    let with_walk = b.call(
1596                        "join_builds",
1597                        vec![
1598                            with_plinth,
1599                            list(vec![build(
1600                                "box",
1601                                vec![
1602                                    ("at", vec3(fx(0.0), fx(0.04), add(var(7), fx(7.0)))),
1603                                    ("w", fx(4.0)),
1604                                    ("h", fx(0.12)),
1605                                    ("d", fx(14.0)),
1606                                    ("solid", Term::Bool(false)),
1607                                    ("name", txt("walkway")),
1608                                ],
1609                            )]),
1610                        ],
1611                    );
1612                    // one corridor per title
1613                    Term::Fold {
1614                        cap: 8,
1615                        list: Box::new(Term::Iota {
1616                            cap: 8,
1617                            count: Box::new(p1(PrimOp::Len, var(3))),
1618                        }),
1619                        init: Box::new(with_walk),
1620                        // [i=0, acc=1, sky=2, stone=3, style=4, titles=5,
1621                        //  wing_len=6, columns=7, height=8, radius=9, name=10]
1622                        body: Box::new(b.call(
1623                            "join_builds",
1624                            vec![
1625                                var(1),
1626                                b.call(
1627                                    "wing",
1628                                    vec![
1629                                        b.call("wing_az", vec![var(0)]),
1630                                        var(9),
1631                                        var(6),
1632                                        fx(6.8),
1633                                        // the same wall height as the
1634                                        // rotunda: a gallery taller than
1635                                        // the room it leaves is a mistake
1636                                        // you feel before you see
1637                                        mul(var(8), fx(0.7)),
1638                                    ],
1639                                ),
1640                            ],
1641                        )),
1642                    }
1643                }),
1644                (
1645                    "needs",
1646                    b.call(
1647                        "join",
1648                        vec![
1649                            b.call(
1650                                "gated",
1651                                vec![
1652                                    b.call(
1653                                        "styled",
1654                                        vec![
1655                                            b.call(
1656                                                "colonnade",
1657                                                // On the wall line, half a metre
1658                                                // proud of its face: engaged
1659                                                // columns, the way a colonnade
1660                                                // and a wall actually meet.
1661                                                vec![var(5), add(var(7), fx(0.45)), var(6)],
1662                                            ),
1663                                            var(2),
1664                                        ],
1665                                    ),
1666                                    gate_azimuths(var(3)),
1667                                    fx(13.0),
1668                                ],
1669                            ),
1670                            b.call(
1671                                "join",
1672                                vec![
1673                                    // every gate framed by its own pair
1674                                    b.call(
1675                                        "styled",
1676                                        vec![
1677                                            b.call(
1678                                                "jambs",
1679                                                vec![
1680                                                    gate_azimuths(var(3)),
1681                                                    add(var(7), fx(0.45)),
1682                                                    var(6),
1683                                                    fx(14.0),
1684                                                ],
1685                                            ),
1686                                            var(2),
1687                                        ],
1688                                    ),
1689                                    b.call(
1690                                        "join",
1691                                        vec![
1692                                            list(vec![need(
1693                                                "vessel",
1694                                                vec![
1695                                                    ("at", vec3(fx(0.0), fx(0.6), fx(0.0))),
1696                                                    ("w", fx(1.2)),
1697                                                    ("h", fx(1.6)),
1698                                                    ("d", fx(1.2)),
1699                                                    ("tol", fx(0.8)),
1700                                                    ("style", var(2)),
1701                                                    ("name", txt("subject")),
1702                                                    ("solid", Term::Bool(false)),
1703                                                ],
1704                                            )]),
1705                                            // a sparser outer colonnade: the precinct
1706                                            // wall gets a rhythm too, at a scale you
1707                                            // read from across the grounds
1708                                            b.call(
1709                                                "labelled",
1710                                                vec![
1711                                                    b.call(
1712                                                        "gated",
1713                                                        vec![
1714                                                            b.call(
1715                                                                "styled",
1716                                                                vec![
1717                                                                    b.call(
1718                                                                        "colonnade",
1719                                                                        vec![
1720                                                                            int(12),
1721                                                                            add(
1722                                                                                add(
1723                                                                                    add(
1724                                                                                        var(7),
1725                                                                                        var(4),
1726                                                                                    ),
1727                                                                                    fx(8.0),
1728                                                                                ),
1729                                                                                fx(0.45),
1730                                                                            ),
1731                                                                            mul(var(6), fx(0.9)),
1732                                                                        ],
1733                                                                    ),
1734                                                                    var(2),
1735                                                                ],
1736                                                            ),
1737                                                            list(vec![fx(180.0)]),
1738                                                            fx(9.0),
1739                                                        ],
1740                                                    ),
1741                                                    txt("outer "),
1742                                                ],
1743                                            ),
1744                                        ],
1745                                    ),
1746                                ],
1747                            ),
1748                        ],
1749                    ),
1750                ),
1751                (
1752                    "veils",
1753                    // one at the end of each wing, facing back down it
1754                    Term::Fold {
1755                        cap: 8,
1756                        list: Box::new(var(3)),
1757                        init: Box::new(none_of(rec(vec![
1758                            ("at", vec3(fx(0.0), fx(0.0), fx(0.0))),
1759                            ("label", txt("")),
1760                            ("to", txt("")),
1761                            ("yaw", fx(0.0)),
1762                        ]))),
1763                        // [title=0, acc=1, …, wing_len=6, …, radius=9]
1764                        body: Box::new(Term::Let(
1765                            Box::new(b.call("wing_az", vec![p1(PrimOp::Len, var(1))])),
1766                            // [az=0, i=1, acc=2, sky=3, stone=4, style=5,
1767                            //  titles=6, wing_len=7, columns=8, height=9, radius=10]
1768                            Box::new(Term::Let(
1769                                Box::new(add(var(10), sub(var(7), fx(1.2)))),
1770                                // [reach=0, az=1, i=2, acc=3, …]
1771                                Box::new(p2(
1772                                    PrimOp::ListCat,
1773                                    var(3),
1774                                    list(vec![rec(vec![
1775                                        (
1776                                            "at",
1777                                            vec3(
1778                                                mul(
1779                                                    var(0),
1780                                                    p1(
1781                                                        PrimOp::FSin,
1782                                                        mul(
1783                                                            div(var(1), fx(360.0)),
1784                                                            Term::Fix(FIX_TAU),
1785                                                        ),
1786                                                    ),
1787                                                ),
1788                                                fx(1.4),
1789                                                sub(
1790                                                    fx(0.0),
1791                                                    mul(
1792                                                        var(0),
1793                                                        p1(
1794                                                            PrimOp::FCos,
1795                                                            mul(
1796                                                                div(var(1), fx(360.0)),
1797                                                                Term::Fix(FIX_TAU),
1798                                                            ),
1799                                                        ),
1800                                                    ),
1801                                                ),
1802                                            ),
1803                                        ),
1804                                        // face back down the corridor
1805                                        ("yaw", add(fx(180.0), sub(fx(0.0), var(1)))),
1806                                        ("to", txt("")),
1807                                        // the door says where it goes
1808                                        ("label", var(2)),
1809                                    ])]),
1810                                )),
1811                            )),
1812                        )),
1813                    },
1814                ),
1815                (
1816                    "spawns",
1817                    list(vec![rec(vec![
1818                        ("at", vec3(fx(0.0), fx(0.0), mul(var(7), fx(0.62)))),
1819                        ("yaw", fx(180.0)),
1820                        ("name", txt("entry")),
1821                    ])]),
1822                ),
1823                (
1824                    "lights",
1825                    p2(
1826                        PrimOp::ListCat,
1827                        b.call("lamp_ring", vec![int(6), mul(var(7), fx(0.62))]),
1828                        // A corridor lit only from the room it leaves is a
1829                        // corridor nobody walks down.
1830                        Term::Fold {
1831                            cap: 8,
1832                            list: Box::new(Term::Iota {
1833                                cap: 8,
1834                                count: Box::new(p1(PrimOp::Len, var(3))),
1835                            }),
1836                            init: Box::new(none_of(rec(vec![
1837                                ("at", vec3(fx(0.0), fx(0.0), fx(0.0))),
1838                                ("fixture", Term::Bool(true)),
1839                                ("intensity", fx(1.0)),
1840                                ("range", fx(LAMP_RANGE)),
1841                                ("warm", fx(1.0)),
1842                            ]))),
1843                            // [i=0, acc=1, sky=2, stone=3, style=4, titles=5,
1844                            //  wing_len=6, columns=7, height=8, radius=9, name=10]
1845                            body: Box::new(Term::Let(
1846                                Box::new(mul(
1847                                    div(b.call("wing_az", vec![var(0)]), fx(360.0)),
1848                                    Term::Fix(FIX_TAU),
1849                                )),
1850                                // [theta=0, i=1, acc=2, …, wing_len=7, …, radius=10]
1851                                Box::new(Term::Let(
1852                                    Box::new(add(var(10), mul(var(7), fx(0.55)))),
1853                                    // [d=0, theta=1, i=2, acc=3, …]
1854                                    Box::new(p2(
1855                                        PrimOp::ListCat,
1856                                        var(3),
1857                                        list(vec![rec(vec![
1858                                            (
1859                                                "at",
1860                                                vec3(
1861                                                    mul(var(0), p1(PrimOp::FSin, var(1))),
1862                                                    fx(2.4),
1863                                                    sub(
1864                                                        fx(0.0),
1865                                                        mul(var(0), p1(PrimOp::FCos, var(1))),
1866                                                    ),
1867                                                ),
1868                                            ),
1869                                            ("fixture", Term::Bool(true)),
1870                                            ("intensity", fx(1.0)),
1871                                            ("range", fx(LAMP_RANGE)),
1872                                            ("warm", fx(1.0)),
1873                                        ])]),
1874                                    )),
1875                                )),
1876                            )),
1877                        },
1878                    ),
1879                ),
1880                (
1881                    "signs",
1882                    // Each wing's name over its gate, facing the rotunda —
1883                    // read on approach, which is the only moment it helps.
1884                    Term::Fold {
1885                        cap: 8,
1886                        list: Box::new(var(3)),
1887                        init: Box::new(none_of(rec(vec![
1888                            ("at", vec3(fx(0.0), fx(0.0), fx(0.0))),
1889                            ("h", fx(1.5)),
1890                            ("text", txt("")),
1891                            ("w", fx(2.0)),
1892                            ("yaw", fx(0.0)),
1893                        ]))),
1894                        // Fold with a counting accumulator would need a record;
1895                        // instead the index rides in the accumulator's length.
1896                        // [title=0, acc=1, sky=2, stone=3, style=4, titles=5,
1897                        //  wing_len=6, columns=7, height=8, radius=9, name=10]
1898                        body: Box::new(Term::Let(
1899                            Box::new(b.call("wing_az", vec![p1(PrimOp::Len, var(1))])),
1900                            // [az=0, title=1, acc=2, …, radius=10]
1901                            Box::new(p2(
1902                                PrimOp::ListCat,
1903                                var(2),
1904                                list(vec![rec(vec![
1905                                    (
1906                                        "at",
1907                                        vec3(
1908                                            mul(
1909                                                add(var(10), fx(0.5)),
1910                                                p1(
1911                                                    PrimOp::FSin,
1912                                                    mul(div(var(0), fx(360.0)), Term::Fix(FIX_TAU)),
1913                                                ),
1914                                            ),
1915                                            add(mul(var(9), fx(0.7)), fx(0.45)),
1916                                            sub(
1917                                                fx(0.0),
1918                                                mul(
1919                                                    add(var(10), fx(0.5)),
1920                                                    p1(
1921                                                        PrimOp::FCos,
1922                                                        mul(
1923                                                            div(var(0), fx(360.0)),
1924                                                            Term::Fix(FIX_TAU),
1925                                                        ),
1926                                                    ),
1927                                                ),
1928                                            ),
1929                                        ),
1930                                    ),
1931                                    // face the rotunda: ψ = −az
1932                                    ("yaw", sub(fx(0.0), var(0))),
1933                                    ("text", var(1)),
1934                                    ("w", fx(3.6)),
1935                                    ("h", fx(0.8)),
1936                                ])]),
1937                            )),
1938                        )),
1939                    },
1940                ),
1941            ]),
1942        ),
1943    );
1944
1945    let names: Vec<String> = b.by_name.keys().cloned().collect();
1946    let exports: Vec<(&str, usize)> = names.iter().map(|n| (n.as_str(), b.idx(n))).collect();
1947    Package::build("weft-draft", b.defs.clone(), exports).expect("the drafting library builds")
1948}
1949
1950/// Lamp reach, mirroring `infinite_manifest::plan::metric::LAMP_SPACING`'s
1951/// companion. The language crate does not depend on the manifest format, so
1952/// the number is written in both places deliberately — a change to either
1953/// shows up as a diff instead of drifting quietly.
1954const LAMP_RANGE: f32 = 9.0;
1955
1956#[cfg(test)]
1957mod tests {
1958    use super::*;
1959    use crate::{eval_call, pack::link, Value};
1960
1961    fn call(pkg: &Package, name: &str, args: Vec<Value>) -> Value {
1962        let hash = pkg
1963            .export(name)
1964            .unwrap_or_else(|| panic!("export '{name}'"));
1965        let entry = pkg.defs.get(&hash).cloned().expect("def");
1966        let module = link(&[pkg.clone()], vec![entry], 0).expect("links");
1967        eval_call(&module, module.entry, args, 60_000_000)
1968            .unwrap_or_else(|e| panic!("'{name}' failed: {e:?}"))
1969            .value
1970    }
1971    fn f(v: f32) -> Value {
1972        Value::Fix((v * FIX_SCALE as f32) as i64)
1973    }
1974    fn t(s: &str) -> Value {
1975        Value::Text(s.into())
1976    }
1977    /// The same seam chisel uses: Weft values cross to JSON, and the plan
1978    /// format reads them back.
1979    fn to_json(v: &Value) -> serde_json::Value {
1980        match v {
1981            Value::Int(i) => serde_json::Value::from(*i),
1982            Value::Fix(raw) => serde_json::Value::from(*raw as f64 / FIX_SCALE as f64),
1983            Value::Bool(b) => serde_json::Value::Bool(*b),
1984            Value::Text(s) => serde_json::Value::String(s.clone()),
1985            Value::List(xs) => serde_json::Value::Array(xs.iter().map(to_json).collect()),
1986            Value::Rec(fs) => {
1987                serde_json::Value::Object(fs.iter().map(|(k, x)| (k.clone(), to_json(x))).collect())
1988            }
1989            Value::Action { .. } => serde_json::Value::Null,
1990        }
1991    }
1992
1993    #[test]
1994    fn the_drafting_library_verifies() {
1995        let pkg = package();
1996        pkg.verify()
1997            .expect("every figure type-checks and terminates");
1998        for must in [
1999            "need",
2000            "join",
2001            "ring_of",
2002            "row_of",
2003            "colonnade",
2004            "hall",
2005            "courtyard",
2006        ] {
2007            assert!(pkg.exports.contains_key(must), "exports {must}");
2008        }
2009    }
2010
2011    #[test]
2012    fn a_hall_lays_itself_out_soundly() {
2013        let pkg = package();
2014        let v = call(
2015            &pkg,
2016            "hall",
2017            vec![
2018                t("Test Hall"),
2019                f(14.0),
2020                f(5.2),
2021                Value::Int(12),
2022                t("classical"),
2023                t("marble"),
2024                t("dusk"),
2025            ],
2026        );
2027        let json = to_json(&v);
2028        let plan: infinite_manifest::plan::Plan =
2029            serde_json::from_value(json).expect("a hall is a plan");
2030        assert_eq!(plan.name, "Test Hall");
2031        assert_eq!(plan.palette.style, "classical");
2032        // Twelve columns on the ring, plus the threshold arch.
2033        assert_eq!(plan.needs.iter().filter(|n| n.kind == "column").count(), 12);
2034        assert!(plan.needs.iter().any(|n| n.kind == "arch"));
2035        // Columns sit on the ring, sized to it, and face the centre.
2036        for n in plan.needs.iter().filter(|n| n.kind == "column") {
2037            let r = (n.at[0] * n.at[0] + n.at[2] * n.at[2]).sqrt();
2038            assert!((r - 14.0 * 0.82).abs() < 0.2, "on the ring: {r}");
2039            assert!((n.h - 5.2).abs() < 0.01, "height {}", n.h);
2040            assert_eq!(n.style, "classical", "the palette reached the needs");
2041        }
2042        // And the layout passes its own checks — floor, light, clear spawn.
2043        assert!(plan.check().is_empty(), "sound hall: {:?}", plan.check());
2044    }
2045
2046    #[test]
2047    fn a_museum_hall_drafts_the_architecture_the_generator_hand_wrote() {
2048        let pkg = package();
2049        let titles = ["Ascent", "Empire", "Exile", "Legacy"];
2050        let v = call(
2051            &pkg,
2052            "museum",
2053            vec![
2054                t("Napoleon"),
2055                f(16.0),
2056                f(3.8),
2057                Value::Int(24),
2058                f(18.0),
2059                Value::List(titles.iter().map(|s| t(s)).collect()),
2060                t("classical"),
2061                t("marble"),
2062                t("dusk"),
2063            ],
2064        );
2065        let plan: infinite_manifest::plan::Plan =
2066            serde_json::from_value(to_json(&v)).expect("a museum is a plan");
2067        // The rotunda: a floor, a walled ring with openings, four wings.
2068        assert!(plan.builds.iter().any(|b| b.shape == "disc"), "a floor");
2069        let walls = plan.builds.iter().filter(|b| b.name == "wall").count();
2070        assert!(
2071            (12..=20).contains(&walls),
2072            "five gates open two segments each, leaving most of the ring standing; got {walls}"
2073        );
2074        assert_eq!(
2075            plan.builds
2076                .iter()
2077                .filter(|b| b.name == "wing floor")
2078                .count(),
2079            4
2080        );
2081        assert_eq!(
2082            plan.builds.iter().filter(|b| b.name == "wing wall").count(),
2083            8
2084        );
2085        assert_eq!(plan.builds.iter().filter(|b| b.name == "lintel").count(), 4);
2086        // Wall segments stand ON the ring, tangent to it.
2087        for w in plan.builds.iter().filter(|b| b.name == "wall") {
2088            let r = (w.at[0] * w.at[0] + w.at[2] * w.at[2]).sqrt();
2089            assert!((r - 16.0).abs() < 0.05, "on the ring: {r}");
2090        }
2091        // The gates are real holes: nothing stands across the entrance (az
2092        // 180, +z) or across a wing mouth (az ±55, ±125).
2093        for gate in [180.0f32, 55.0, -55.0, 125.0, -125.0] {
2094            let rad = gate.to_radians();
2095            let (gx, gz) = (16.0 * rad.sin(), -16.0 * rad.cos());
2096            let nearest = plan
2097                .builds
2098                .iter()
2099                .filter(|b| b.name == "wall")
2100                .map(|b| ((b.at[0] - gx).powi(2) + (b.at[2] - gz).powi(2)).sqrt())
2101                .fold(f32::MAX, f32::min);
2102            assert!(
2103                nearest > 3.0,
2104                "gate at {gate}° is open (nearest wall {nearest:.1} m)"
2105            );
2106        }
2107        // A veil at the end of every wing, and the colonnade inside the walls.
2108        assert_eq!(plan.veils.len(), 4);
2109        // Not "24 columns" — *no column in a doorway*. A gated ring drops the
2110        // ones that would stand in the opening, so the count follows from the
2111        // gates, and pinning it would only pin the bug back in.
2112        let cols: Vec<_> = plan.needs.iter().filter(|n| n.name == "column").collect();
2113        assert!(
2114            (8..=24).contains(&cols.len()),
2115            "a colonnade remains: {}",
2116            cols.len()
2117        );
2118        for gate in [180.0f32, 55.0, -55.0, 125.0, -125.0] {
2119            let rad = gate.to_radians();
2120            let (gx, gz) = (16.45 * rad.sin(), -16.45 * rad.cos());
2121            let nearest = cols
2122                .iter()
2123                .map(|c| ((c.at[0] - gx).powi(2) + (c.at[2] - gz).powi(2)).sqrt())
2124                .fold(f32::MAX, f32::min);
2125            assert!(
2126                nearest > 3.0,
2127                "gate at {gate}° is walkable (nearest column {nearest:.1} m)"
2128            );
2129        }
2130        // The precinct: grounds, a boundary wall, and a colonnade at a scale
2131        // you read from across it.
2132        assert_eq!(
2133            plan.needs
2134                .iter()
2135                .filter(|n| n.name == "outer column")
2136                .count(),
2137            12
2138        );
2139        assert!(
2140            plan.builds.iter().any(|b| b.name == "grounds"),
2141            "somewhere to stand"
2142        );
2143        let outer = plan
2144            .builds
2145            .iter()
2146            .filter(|b| b.name == "outer wall")
2147            .count();
2148        assert!(
2149            (28..=32).contains(&outer),
2150            "a wall around the grounds; got {outer}"
2151        );
2152        for w in plan.builds.iter().filter(|b| b.name == "outer wall") {
2153            let r = (w.at[0] * w.at[0] + w.at[2] * w.at[2]).sqrt();
2154            // Weft's sine is Bhaskara's, ~0.2 % — good enough to build with,
2155            // and the tolerance says so out loud rather than pretending.
2156            // radius + wing + 8 m of walking room = 42, the same figure the
2157            // hand-written hall arrived at — derived here instead of chosen.
2158            assert!((r - 42.0).abs() < 0.15, "on the outer ring: {r}");
2159        }
2160        assert!(
2161            plan.needs.iter().any(|n| n.name == "subject"),
2162            "something at the focus"
2163        );
2164        // The museum says its own words: one named gate per section, hung over
2165        // that section's mouth and turned to face the rotunda you read it from.
2166        assert_eq!(plan.signs.len(), 4, "a name over every gate");
2167        for (i, title) in titles.iter().enumerate() {
2168            let sign = plan.signs.iter().find(|s| s.text == *title).expect(title);
2169            let az = if i % 2 == 0 {
2170                55.0 + 70.0 * (i / 2) as f32
2171            } else {
2172                -(55.0 + 70.0 * (i / 2) as f32)
2173            };
2174            let rad: f32 = az.to_radians();
2175            let (gx, gz) = (16.5 * rad.sin(), -16.5 * rad.cos());
2176            assert!(
2177                (sign.at[0] - gx).abs() < 0.05 && (sign.at[2] - gz).abs() < 0.05,
2178                "'{title}' hangs over its own gate, not someone else's: {:?}",
2179                sign.at
2180            );
2181            assert!(
2182                (sign.yaw - -az).abs() < 0.05,
2183                "read from inside the rotunda"
2184            );
2185        }
2186        // Copings cap the wall, and the walk in is stone under the arrival.
2187        assert!(
2188            plan.builds.iter().any(|b| b.name == "walkway"),
2189            "arrivals land on stone"
2190        );
2191        assert_eq!(
2192            plan.builds.iter().filter(|b| b.name == "coping").count(),
2193            walls,
2194            "every standing segment is capped"
2195        );
2196        assert!(plan.check().is_empty(), "sound museum: {:?}", plan.check());
2197    }
2198
2199    #[test]
2200    fn a_courtyard_lays_itself_out_soundly() {
2201        let pkg = package();
2202        let v = call(
2203            &pkg,
2204            "courtyard",
2205            vec![t("Yard"), f(24.0), t("rustic"), t("sandstone"), t("day")],
2206        );
2207        let plan: infinite_manifest::plan::Plan =
2208            serde_json::from_value(to_json(&v)).expect("a courtyard is a plan");
2209        assert_eq!(plan.needs.iter().filter(|n| n.kind == "column").count(), 10);
2210        assert!(
2211            plan.needs.iter().any(|n| n.name == "focus"),
2212            "something to walk toward"
2213        );
2214        assert!(plan.check().is_empty(), "sound yard: {:?}", plan.check());
2215    }
2216}