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}