axstack 0.36.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +5 -4
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/host-operations.md +9 -5
  5. package/docs/installation.md +20 -41
  6. package/docs/workflows.md +12 -9
  7. package/package.json +1 -1
  8. package/profiles/presets/claude-only.json +3 -3
  9. package/profiles/presets/codex-only.json +3 -3
  10. package/profiles/presets/mixed.json +5 -4
  11. package/skills/axstack/references/automations.md +7 -2
  12. package/skills/axstack/references/lifecycle.md +2 -1
  13. package/skills/axstack/references/review-manager-prompt.md +44 -3
  14. package/skills/axstack/references/role-roster.md +13 -6
  15. package/skills/axstack/references/routing.md +2 -1
  16. package/skills/axstack/references/run-record.md +2 -0
  17. package/skills/axstack/references/ui-verification.md +6 -10
  18. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  19. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  20. package/skills/axstack-anatomy/LICENSE +21 -0
  21. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  22. package/skills/axstack-anatomy/README.md +316 -0
  23. package/skills/axstack-anatomy/SKILL.md +74 -0
  24. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  25. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  26. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  27. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  28. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  29. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  30. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  31. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  32. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  33. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  34. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  35. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  36. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  37. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  38. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  41. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  42. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  43. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  44. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  45. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  46. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  47. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  48. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  49. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  50. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  51. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  52. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  53. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  54. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  55. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  56. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  57. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  58. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  59. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  60. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  61. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  62. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  63. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  64. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  65. package/skills/axstack-anatomy/kit/iso.css +106 -0
  66. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  67. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  68. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  69. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  70. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  71. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  72. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  73. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  74. package/skills/axstack-anatomy/references/3d.md +352 -0
  75. package/skills/axstack-anatomy/references/craft.md +325 -0
  76. package/skills/axstack-anatomy/references/kit.md +419 -0
  77. package/skills/axstack-anatomy/references/motion.md +165 -0
  78. package/skills/axstack-anatomy/references/react.md +42 -0
  79. package/skills/axstack-anatomy/references/verify.md +351 -0
  80. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  81. package/skills/axstack-anatomy/references/webgl.md +473 -0
  82. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  83. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  84. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  85. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  86. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  87. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  88. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  89. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  90. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  91. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  92. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  93. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  94. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  95. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  96. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  97. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  98. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  99. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  100. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  101. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  102. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  103. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  104. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  105. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  106. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  107. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  108. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  109. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  110. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  111. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  112. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  113. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  114. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  115. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  116. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  117. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  118. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  119. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  120. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  121. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  122. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  123. package/skills/axstack-anatomy/theme.js +56 -0
  124. package/skills/axstack-explain/SKILL.md +28 -15
  125. package/skills/axstack-explain/references/delivery.md +22 -0
  126. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  127. package/skills/axstack-explain/references/inline-pages.md +8 -8
  128. package/skills/axstack-explain/references/visual-qa.md +3 -5
  129. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  130. package/skills/axstack-research/SKILL.md +18 -6
  131. package/src/archify-legacy.js +70 -0
  132. package/src/capabilities.js +1 -29
  133. package/src/installer.js +11 -31
  134. package/skills/axstack-diagram/SKILL.md +0 -28
  135. package/skills/axstack-diagram/references/archify.md +0 -76
  136. package/src/archify-pin.js +0 -5
  137. package/src/archify.js +0 -151
@@ -0,0 +1,165 @@
1
+ # Motion and interaction
2
+
3
+ A figure is alive when its mechanism moves the way the real thing would: a carriage glides and settles, a spring overshoots once, a needle swings to the new reading. Motion should be calm enough to follow and true to the model.
4
+
5
+ ## Principles
6
+ - **Time-based.** Take `dt` from `requestAnimationFrame` timestamps and clamp it to 1/30 s so a background tab doesn't jump. Never step a fixed amount per frame.
7
+ - **Ease toward targets.** Pointer and keyboard set a *target*, and the part follows with exponential easing (`x += (target − x)·(1 − e^(−dt/τ))`, τ = 0.1–0.3 s) or a spring. Parts never snap to the cursor.
8
+ - **Real springs where the subject is springy.** Use stiffness and damping from the real model, sub-stepped about 240 times a second for stability. Snap to the target once both error and velocity fall below 5e-4.
9
+ - **One loop per figure.** A single `tick(dt)` updates the model, then writes the DOM through refs: `setAttribute("transform" | "d")`, `textContent`, `dataset`. Don't re-render React per frame.
10
+ - **Stop when settled or offscreen.** Use an `IntersectionObserver` with rootMargin `120px 0px`, and read the newest entry (`entries[entries.length - 1]`): when the stage leaves and comes back between two callbacks, one callback delivers both entries oldest first, and reading the first one leaves the figure frozen on screen. Stop the loop once everything has reached its target.
11
+ - **Reduced motion.** When `prefers-reduced-motion: reduce` is set, jump to targets with no tour, flow or pulse. The figure must still respond to input.
12
+ - **Idle tour.** When nobody is interacting, play a scripted loop after about 1.4 s that shows the point of the figure. Pause it on hover, focus or press, and resume about 3.6 s after the last interaction. The tour drives the same targets the user would.
13
+ - **Readout updates live** from the same model, in true units and short: `block 2 · reads 14.0 px`, `120.0 × 32.0 · lean 10.6 px`. Read it from where the parts are, not from the target: the Raptor says `taking apart` or `closing up` while any spring is moving or pending, and only then `apart · 11 assemblies` or `1,630 kg · cold`. Take a phase word and the number beside it from the same instant of the same source: a blind-test bench printed `end of stroke · relief 85 bar` (the word from the model, the number from a gauge needle lagging 0.25 s behind it) although its relief valve cracks at 92, and `idle · 44 bar` just after retracting.
14
+
15
+ ## Pointer
16
+ - Map the pointer to world space with the screen vector of an axis. Use `axisVector(P, [1, 0, 0])` for motion along x, or map the figure's width fraction onto the part's travel range. The range mapping is simpler and feels better when the travel is short.
17
+ - Hover previews and press commits. On touch there is no hover, so make a tap act and keep `touch-action: pan-y` so the page still scrolls.
18
+ - Use pointer capture while dragging, and release it on `pointerup` or `pointercancel`.
19
+
20
+ ## Keyboard and accessibility
21
+ - The figure's root is focusable (`tabIndex=0`) with `role="slider"` (or `"group"` plus buttons when there are discrete choices), an `aria-label` that explains the controls, and `aria-valuenow`/`aria-valuetext` in words, such as "Pressed: 129.6 by 34.6 pixels, leaning 10.6 px". Write the value text when the reader's choice changes (a part selected, apart or assembled, firing or cold), not from the loop: screen readers announce every change of a focused slider's value text, so a readout ticking each frame ("Pc 290 bar · 222 tf") became a stream of numbers. Keep the visual readout and the accessible value separate (the Raptor's `say()` writes only the readout, `describe()` the value text); the live region carries the phases.
22
+ - Keys: arrows step by a sensible unit (Shift for bigger steps), Home and End go to the extremes, Space or Enter presses or toggles, and Escape releases.
23
+ - Show a focus ring: `outline: 1px solid var(--anatomy-muted); outline-offset: 6px`.
24
+ - The `<svg>` carries `role="img"` and an `aria-label` describing the object and what it shows.
25
+
26
+ ## Vanilla loop (framework-free)
27
+ ```js
28
+ let frame = 0, last = 0, visible = false, touring = !still.matches, clock = 0;
29
+ function tick(now) {
30
+ const dt = last ? Math.min((now - last) / 1000, 1 / 30) : 1 / 60;
31
+ last = now;
32
+ const calm = still.matches;
33
+ if (touring) { clock += dt; target = tourAt(clock); }
34
+ x = calm ? target : x + (target - x) * (1 - Math.exp(-dt / 0.28));
35
+ springTo(tip, heightUnder(x), dt, 420, 26, calm);
36
+ draw(); // setAttribute on refs, readout.textContent = …
37
+ const settled = !touring && x === target && tip.rate === 0;
38
+ frame = visible && !settled ? requestAnimationFrame(tick) : 0;
39
+ }
40
+ const run = () => { if (!frame && visible) { last = 0; frame = requestAnimationFrame(tick); } };
41
+ new IntersectionObserver((entries) => { visible = entries[entries.length - 1].isIntersecting; if (visible) run(); }, { rootMargin: "120px 0px" }).observe(stage);
42
+ ```
43
+ `examples/dial-indicator/build.mjs` contains a complete version, with the tour, pointer mapping and keyboard stops.
44
+
45
+ ## React loop
46
+ Keep static art in props built on the server and move only `<g>` wrappers and the paths that change shape:
47
+
48
+ ```tsx
49
+ "use client";
50
+ export function RigLive(props: { base: React.ReactNode; carriage: React.ReactNode; dial: React.ReactNode }) {
51
+ const { anchor, say } = useReadout<HTMLDivElement>();
52
+ const car = React.useRef<SVGGElement>(null);
53
+ const needle = React.useRef<SVGPathElement>(null);
54
+ const state = React.useRef({ x: REST, target: REST, tip: { at: 0, rate: 0 } });
55
+ const visible = useInView(anchor);
56
+ const kick = useLoop((dt, calm) => {
57
+ const s = state.current;
58
+ s.x = follow(s.x, s.target, dt, 0.28, calm);
59
+ const settled = spring(s.tip, heightUnder(s.x), dt, 420, 26, calm);
60
+ car.current?.setAttribute("transform", translateAlong(P, [s.x, 0, 0]));
61
+ needle.current?.setAttribute("d", needleAt(s.tip.at));
62
+ say(readoutOf(s));
63
+ return !(settled && s.x === s.target);
64
+ }, visible);
65
+ return (
66
+ <div ref={anchor} tabIndex={0} role="slider" onPointerMove={(e) => { state.current.target = toWorld(e); kick(); }}>
67
+ <IsoFigure width={W} height={H} label={LABEL}>
68
+ {props.base}
69
+ <g ref={car}>{props.carriage}</g>
70
+ {props.dial}
71
+ <path ref={needle} className="iso-line" data-tone="lit" d={NEEDLE_REST} />
72
+ </IsoFigure>
73
+ </div>
74
+ );
75
+ }
76
+ ```
77
+ `examples/test-rig/live.tsx` is the full production version, with springs, a tour, press and pull, keyboard handling and aria.
78
+
79
+ ## Shapes that change
80
+ When a part changes shape rather than position (a swelling capsule, a compressing spring, a needle turning), rebuild only its paths each frame with the kit, using `slabOf`, `coil` or `lineOnTop`, and write them to its paths: `fill`, `shades[0..3]`, `top`, `crease`, `bevel`, `outline`. One solid costs well under 0.1 ms. Measure it: a frame should stay under about 1 ms of script.
81
+ - **Precomputed frames step.** A lever drawn from 41 prebuilt frames over ±15° moved its knob about 0.8 px per frame, visible in slow motion. Rebuild a rotating part each frame, or precompute enough frames that one step moves nothing more than 0.25 px.
82
+ - **A rod that slides out of a gland** doesn't change shape: it is one full-length rod that moves, clipped at the gland face by a clip that stays put (`G.rodOut`, `holdStill`, `craft.md`, section 3). Never draw it as several overlapping copies.
83
+
84
+ ## Envelopes for one-shot events
85
+ A flash, a click, a pop, a shake or a shock is an event with an age, not a value you set. Drive it with an envelope that has an attack and a decay. `kit/gl.mjs` exports `smooth(a, b, x)` (smoothstep) and `burst(age, rise, fall)`, which is `smooth(0, rise, age)` times `exp(−(age − rise)/fall)` after the peak and 0 before the event:
86
+ ```js
87
+ import { smooth, burst, settle } from "../../kit/gl.mjs";
88
+ const flash = calm ? 0 : burst(clock - mainAt, 0.08, 0.13);
89
+ ```
90
+ On a standalone page `glScript()` already inlines all three, so use them directly and don't declare your own: a second `const smooth` in the same module script is a SyntaxError.
91
+ - The attack is 40–120 ms, and the smoothstep gives it zero slope at onset. Setting a value to 1 in one frame is a pop.
92
+ - Store the event's start time (`mainAt = clock`) and compute every envelope from its age each frame. Two events that overlap then simply add.
93
+ - Give each event its own flag (`popped`), so a quick restart can't swallow the next one and can't cut off one that is still fading.
94
+ - Under reduced motion every event envelope is 0.
95
+
96
+ ## Critically damped settle
97
+ For a value that should arrive without overshoot (a hue, a brightness, a lamp, a reach, a frequency), use the kit's `settle(state, target, dt, seconds, calm)`: a critically damped spring on `state = { x, v }` with time constant `seconds`, `v += (w²·(target − x) − 2w·v)·dt` with `w = 1/seconds`, in four sub-steps. It is smoother than exponential follow, because velocity is continuous too. It snaps to the target once error and velocity are both below 1e-4, jumps there under `calm`, and returns true while still moving, so it can keep the loop alive:
98
+ ```js
99
+ const moving = settle(freq, state.tf, dt, 0.625, calm);
100
+ ```
101
+ The ripple tank runs its frequency (0.625 s) and dipper spacing (0.4 s) through `settle`, so a key press or a pointer move never jumps the wavelength.
102
+ - **Separate what a phase changes.** When one physical quantity (say chamber pressure) drives colour, shape and brightness, a fast spring on that quantity makes all three jump at once. Give colour its own slower `settle` (0.3–1 s), and compute brightness so it moves one way through the change.
103
+ - **A fade-out only gets darker.** When a stop begins, freeze the brightness at its current level and scale it only by quantities that fall.
104
+ - **Changing a spring changes its neighbours.** Damping one spring critically slows every phase that relied on its old overshoot, so recheck all of them.
105
+
106
+ ## Exploded views
107
+ Taking a machine apart on hover is a strong way to show its parts (`examples/raptor-engine/`):
108
+ - **Build every piece at its assembled position**, tagged with an assembly name, and give each assembly a world move vector (`PIECES.nozzle.move = [0, 0, -120]`). Moves follow the real assembly: the nozzle drops furthest, side units slide out radially (`out(deg, r, z)`), and the stand doesn't move.
109
+ - **One spring per assembly**, with value 0 for assembled and 1 for apart. The Raptor uses stiffness 64 and drag 14.2, just under critical, so the parts settle with a hint of weight, and sub-steps 4 times per frame.
110
+ - **Stagger the springs.** Opening, each assembly's target changes 35 ms after the previous one, in an order that lets nothing pass through anything (the Raptor: `nozzle, downcomer, controller, plumbing, lox, ftp, hotgas, chamber, orpb, otp, gimbal, stand`, see "Order the stagger" below). Closing uses the reverse order, so the machine folds back together from the inside out. Keep pending targets as `{ value, at }` and apply them when `clock >= at`.
111
+ - **On a reversal, reschedule only what changes.** When the reader reverses mid-way (a quick hover, Space, a tap), cancel the pending target of every assembly that hasn't left yet and stagger only the ones whose target changes, by their index in that shorter list. The last one out is then the first one back, at once. Rescheduling the whole list left the early assemblies flying out toward apart for 0.3 s while later ones sat at home, and the explode only worked because the next assembly moved out of the way 35 ms later: a reversal in that window drove the hot-gas duct into a methane pump that hadn't left. With this rule, shares along the stagger stay in order in every frame (earlier assemblies always lead), which is what the prefix rule below relies on.
112
+ - **Move with transforms.** Each item's screen offset is its move vector, projected and multiplied by the spring value: `translate(tx ty)`. Remove the attribute at 0, so an assembled figure is exactly the static art.
113
+ - **Keep the lines landing.** Parts that join two assemblies (an actuator between the frame and a pump lug, or a cable between a box and a plug) belong to the assembly they physically stay with, and they still end on something when apart. Cables get terminal dots at both ends.
114
+ - **Selection.** Hover, the keyboard arrows and a parts list all select one assembly. The selected parts become the one `lit` accent: CSS on `.it[data-on]` moves `hi` to `lit` and `mid` to `hi`, and switches tops and sides to the lit fills. Every other part steps down one tone (`hi` to `mid`, `mid` to `lo`) through CSS on `[data-focus]`, and the readout says what the selected part does.
115
+
116
+ ## Depth re-sorting of moving parts
117
+ Parts that move toward or away from the viewer change painter's order as they move. When they do, sort every frame:
118
+ - Give every item a static key `k = dot3(ref, viewOf(P)) + bias`, where `ref` is its centre and `bias` settles ties between touching parts. Also give it a lift: `lift = dot3(move, viewOf(P))`.
119
+ - Each frame, its key is `k + lift × spring`. Sort the indices by that key, and **re-append the DOM nodes only when the order actually changed** (one `DocumentFragment`). Most frames change nothing.
120
+ - Store the key and the move vector on the element (`data-k`, `data-m`), so the live script needs no geometry.
121
+ - Tubes are sorted per piece (`tubePieces` → `piece.mid`), so a pipe can pass in front of one part and behind another.
122
+ - **Settle the static keys from the motion, not just the rest poses.** Each item's key is `k + lift × spring`, but with staggered springs every assembly has its own share in any frame, so two items that are ordered right assembled and apart can still be drawn the wrong way round in between: in the first Raptor a pressurisation line hid behind an actuator column it was in front of for 40% of every explode. The Raptor measures true depth for every overlapping pair at 0, 0.1 … 1, at every fourth frame of its own springs replayed with the live stagger until they settle, opening, closing and reversed either side of every stagger boundary, and in the prefix states (`A.explodeStates(STAGGER, SPRING).depth`, which is `[...A.uniformStates(10), ...A.springStates(STAGGER, { ...SPRING, stride: 4, reversals: A.reversalsOf(STAGGER, SPRING.stagger) }), ...A.prefixStates(STAGGER)]`, 1,119 states, and the default of `A.settle(R, P)` for a recorder made with `{ order: STAGGER, spring: SPRING }`), and `orderKeys` solves all of them at once (each rule uses that frame's shares). Opening and closing alone left wrong pairs in the frames a quick hover makes: a purge flange that should sit in front of the chamber band vanished behind it on a reversal at 0.20 s. The audit fails on any wrong pair at rest and in any replayed frame; only the uniform states, which nobody sees, may hold an interlock.
123
+ - **One source for the springs.** Keep the stagger order and the spring constants (`SPRING = { stagger, stiffness, damping, substeps }`) in the build and pass them to the page in its data, so the audit replays exactly what the viewer sees.
124
+ - **Move a part with what it is mounted on (`ride`).** A sensor bolted to the chamber highlights with the plumbing but rides the chamber's spring. Each item records `ride` (the assembly whose spring moves it, found from its move vector) next to `piece` (the assembly it highlights with), the page reads `data-ride`, and `place()` uses `piece[node.ride].x` for both the translate and the sort. An item that borrows another assembly's vector but keeps its own spring slides along its mount and sinks into it mid-flight.
125
+ - **The prefix rule.** Every assembly's path must be clear of every later-leaving assembly parked at home, with the earlier ones out: that is the frame a reversal at a stagger boundary freezes. `A.prefixStates(STAGGER)` poses exactly those (the first `k − 1` out, the `k`-th at 0.25 … 1, the rest home) and the audit checks them with the replayed frames. On the Raptor it moved the hot-gas manifold after the methane pump it bolts to (its vector points into the pump), and the controller before the plumbing (the oxygen-to-preburner line is enclosed by the controller's sense lines, so it can only leave after them).
126
+ - **Order the stagger so nothing has to pass through.** The springs overlap: with 35 ms between starts and about a second to settle, every assembly is moving at once, and relative motion is a blend of every vector. The first Raptor explode passed 66 pairs through each other in mid-flight. The rules that took it to zero, checked every frame and on reversals by the audit:
127
+ - **Whoever is outside leaves first.** On each side, the part that everything else is tucked behind or under moves first: the Raptor's nozzle drops before anything (everything sits on it), and the downcomer, which runs outside every sense line and harness that reaches the methane pump, goes second. Closing reverses it, so the outer part is also the last one home.
128
+ - **When a part's explode vector points into a face of another assembly, that assembly comes earlier in the order.** The downcomer drops 84 onto the nozzle's port tee; the nozzle drops 120 and leaves 35 ms earlier, so the port is always below the flange, opening and closing.
129
+ - **Point every vector away from every face the part touches while the other side is still in place**, not only the two it bolts to: a line threaded between the chamber and the pump must move in the sector both mount normals allow (the Raptor's jacket lines go to −120° with no drop, sliding off both pads), a cable wrapped round a core part may only move in the sector that is outward for its whole wrap (a 90° wrap allows 90°; a wrap over 180° allows nothing, so shorten it or end it on the near side), and a line that would sweep down over a part bolted below it must not drop.
130
+ - **Things threaded through each other share a spring.** Sense lines that rise through the harness ring into a block, and the harness under that block, cannot have separate springs: with 35 ms between them they shear past each other. The Raptor's sense lines and block ride the controller (`BLOCK_MOVE = PIECES.controller.move`, so `ride` resolves to `controller`) while still highlighting as plumbing.
131
+ - **Give each line that is mounted on one part only the move of that part.**
132
+ - **Check the paths a viewer can make**, not only the two from rest: a quick hover reverses the springs halfway (`reversals`), and an assembly that is late on the way back can sweep through a cable that is already home. Reverse at every stagger boundary (`A.reversalsOf`), not at a few round times: the bad windows are about 15 ms wide, and 0.15, 0.3 and 0.5 s missed all of them. Feed those frames to the depth states as well as to the clearance check, and test solids against solids too (`A.solidClearances`): a flange tucked under an overhang on its host met the host's spring tail on the way home, 0.67 deep, with every tube check at zero.
133
+
134
+ ## Turning by hand
135
+ For a 3D figure (`references/3d.md`):
136
+ - **Drag.** `controller.angleAt(clientX, clientY, zFace, "dial")` is the pointer's angle on the face's plane, unwrapped across turns. On pointer down, store it and the current target. On move, set target = start + Δ, and capture the pointer once the move passes about 1.5°. The group follows with exponential follow (τ = 0.12 s). The stage keeps `touch-action: pan-y`.
137
+ - **Keys.** Arrow keys step by one division, Shift steps by ten, Home returns to the index.
138
+ - **Coupled groups.** Drive them from one value through `follow(values)` (a wheel at a quarter of the dial, a finger chain from one `lift`), so a tool, a drag and the tour all agree.
139
+ - **Gestures and clips are pure functions of time** (`tapAt(t)`, `breakAt(t)`): the build samples them, the audit walks them frame by frame, and the page can play them backwards to reset with no jump.
140
+ - **Hook takeover.** Any `window.__isoTurn` call runs `onHold`: stop the tour, and keep values a tool set until the reader acts again.
141
+ - **Half rate.** A dense figure keeps an average of `controller.set` time and renders on alternate frames above 14 ms, every frame below 9 ms, with time-based motion unchanged.
142
+ - Reduced motion gets one still frame. Offscreen, the loop stops (IntersectionObserver).
143
+
144
+ ## Arming
145
+ If an action needs the machine in a certain state (assembled before firing, door closed before spinning), arm first:
146
+ 1. Set the targets that bring it into that state (`setApart(false)`, `select(null)`).
147
+ 2. Mark it armed: the button shows `data-state="armed"` and the readout says `closing up · arming`.
148
+ 3. Start the action's clock only when every spring has arrived and nothing is pending.
149
+
150
+ Never run the action on a half-exploded machine, and never snap the parts home to save time.
151
+
152
+ ## Taking over from the tour
153
+ When the user acts during the idle tour, stop the tour and leave the state where it is. If your take-over helper resets something (for example, it forces the power on), and the user's action is itself a toggle of that thing, the two cancel out in the same frame and replay an animation. Pass the helper a flag (`takeOver(switching)`), and test every toggle in every phase of the tour, not only the default one.
154
+
155
+ A step key pressed during the tour must step from the value the reader sees, not from the tour's target, which may be far ahead. The ripple tank's keys start from `freq.x` and `gap.x` while touring (`state.tf` and `state.td` otherwise), and `steer()` zeroes both springs' velocity when it takes over, so the value only moves in the key's direction.
156
+
157
+ ## Announcing phases
158
+ For a machine with phases, add a visually hidden `aria-live="polite"` element and write only phase changes to it: `arming`, `chill-down, venting`, `preburners lit`, `main stage`, `shutdown`, `cold`. Never write per-frame numbers to it. For a continuous control, wait until the input has been quiet for about 700 ms before you announce (the ripple tank's `announce`). Leave the live region empty at load and fill it with the current state on `focus`, so a screen reader hears the state when it arrives rather than a stale line. A button whose label changes ("Ignite" to "Shut down") doesn't also need `aria-pressed`.
159
+
160
+ ## CSS motion
161
+ - `.iso-line[data-flow]` animates dashes, for things travelling along a path.
162
+ - `.iso-dots circle[data-pulse]` blinks a dot grid slowly, for an "active" surface.
163
+ - For hover lifts, wrap a group with `transition: transform 700ms var(--anatomy-ease)` and translate it up on hover.
164
+
165
+ All of these switch off under reduced motion in `ISO_CSS`.
@@ -0,0 +1,42 @@
1
+ # React and Next.js
2
+
3
+ ## File layout for one figure
4
+ ```
5
+ components/iso/ the kit: iso-kit.ts, iso.css, react/draw.tsx, react/live.tsx
6
+ components/figures/rig/
7
+ model.ts pure model: true numbers, springs, readout text (no DOM)
8
+ view.ts world layout constants, the projection P (fitProjection), helpers like at(point)
9
+ geometry.ts every static path, computed once at module load
10
+ frame.ts paths for the parts that change shape: frameOf(state) -> { glass, coils, needle, ... }
11
+ art.tsx server component: static parts as JSX constants, passed to the live component
12
+ live.tsx "use client": refs, loop, pointer and keyboard, aria, writes the readout
13
+ rig.css scoped tokens and styles (e.g. .rig { … })
14
+ figure.tsx <Plate fig title hint readout keys caption><RigArt /></Plate>
15
+ ```
16
+ `examples/test-rig/` follows this layout. It uses `rig.ts`, `view.ts`, `geometry.ts`, `pose.ts`, `art.tsx`, `live.tsx` and `figure.tsx`.
17
+
18
+ ## The server/client split
19
+ - Geometry is pure TypeScript, so it runs on the server and the client receives finished path strings. No geometry code is shipped unless a moving part needs to rebuild per frame. Then `frame.ts` is imported by `live.tsx` and ships to the client.
20
+ - `art.tsx` builds the static parts as JSX (`<Solid paths={BASE_ART.slab} tone="mid" />` and so on) and passes them as element props, such as `base`, `rail`, `carriage` and `dial`, to the `"use client"` live component. The live component places them in painter's order and wraps the moving ones in `<g ref>`.
21
+ - Only the live component and `kit/react/live.tsx` are client code. `draw.tsx` is server-safe.
22
+
23
+ ## The large-page key-warning trap
24
+ On a big page the server payload is large, and React streams some element props as deferred (lazy) chunks. The client renders `{props.dial}` among siblings. It can't mark a lazy as key-checked, so once the lazy resolves to an element without a key, React logs "Each child in a list should have a unique key", pointing at your live component or at `IsoFigure`. The same figure on a small test page logs nothing, so checking one figure at a time misses it.
25
+
26
+ There are two fixes, and you should do both from the start:
27
+
28
+ 1. **Give the root of every element prop a stable key:** `const DIAL = (<g key="dial">…</g>)`, and `<React.Fragment key="sled">` for fragments. Keys on a single element cost nothing.
29
+ 2. **Keep `"use client"` components out of server-built art.** Every client component element inside a server tree becomes a lazy reference, which makes all of its ancestors lazy too. If parts of the art need interactivity (hover-to-highlight, for example), make them plain `<g data-part="…">` and handle events with delegation in the live component.
30
+
31
+ Any array you build with `.map` needs keys as usual.
32
+
33
+ ## Styles
34
+ - Import `iso.css` once, globally or in the figure. Put `className="iso"` on a wrapper (or `<body>`) for the tokens, plus `data-theme="light"` for the light theme.
35
+ - Scope per-figure tokens to the figure's class (`.rig { --rig-glass-top: … }`) and override kit tokens inside `.iso-plate` only when the card needs it.
36
+ - Don't put figure tokens on `:root`.
37
+
38
+ ## Readout
39
+ `useReadout()` finds the nearest `.iso-plate [data-readout]` from its `anchor`, and `say(text)` writes it without re-rendering. Render the resting text on the server so it is right before hydration.
40
+
41
+ ## Hydration
42
+ Render the rest pose on the server and on the client's first render; start motion only in effects. Don't read `window` while rendering. `asksForStillness()` is safe because it is only called inside effects and loops.
@@ -0,0 +1,351 @@
1
+ # Verify
2
+
3
+ Look at the figure the way a demanding reviewer would, before anyone else does. Rendering it is cheap, and guessing is expensive.
4
+
5
+ ## Capture
6
+ `scripts/capture.mjs` starts its own headless Chrome (set `CHROME_PATH` if it can't find one), loads a file or URL, and clips to `.iso-plate` by default. It also prints the readout, any console errors or exceptions, and whether the page scrolls sideways.
7
+
8
+ ```sh
9
+ node scripts/capture.mjs page.html --out shots/desk.png # desktop 1440, dpr 2
10
+ node scripts/capture.mjs page.html --out shots/phone.png --width 390 # phone
11
+ node scripts/capture.mjs page.html --out shots/hover.png --hover 0.7,0.5 # pointer at 70% across
12
+ node scripts/capture.mjs page.html --out shots/tour.png --times 1200,2400,3600 # motion: extra shots over time
13
+ node scripts/capture.mjs page.html --out shots/keys.png --key ArrowRight,ArrowRight
14
+ node scripts/capture.mjs page.html --out shots/joint.png --zoom 200,80,220,170 # close-up at 4x, in card px
15
+ node scripts/capture.mjs page.html --out shots/still.png --reduced # reduced motion
16
+ node scripts/capture.mjs http://localhost:3000/docs --select "figure:nth-of-type(3) .iso-plate" # one figure on a dev-server page
17
+ ```
18
+ `--select` takes any CSS selector; by default it clips to the first `.iso-plate`. Each `--times` delay counts from the first shot, so `1200,2400,3600` shoots at about 1.2, 2.4 and 3.6 s after it (a capture takes 60–200 ms, so delays closer together than that run late), saved as `name-<ms>.png`. `--key` sends the same key codes as `drive.mjs` (`7` as `Digit7`, `.` as `Period`; write a comma as `Comma`, since the list is comma separated). The other options are `--height` (1000), `--scale` (2), `--wait` (2500 ms before the first shot), `--zoom-scale` (4) and `--theme dark|light`. Read every PNG you make: open it and look.
19
+
20
+ Both scripts start Chrome with WebGL on (`--enable-unsafe-swiftshader --ignore-gpu-blocklist`), so shaders show in `capture.mjs` stills too. Use `drive.mjs` for anything that has to be timed or slowed down.
21
+
22
+ ## Drive
23
+ `scripts/drive.mjs` runs a scripted session in headless Chrome with WebGL on. Use it for sequences: hover, then click, then type, then wait for a phase. Use it also for slowed-down contact sheets that catch pops, and for the no-WebGL fallback. It prints the readout after each shot, then any console errors and exceptions.
24
+
25
+ ```sh
26
+ node scripts/drive.mjs page.html '[["wait",1500],["move",".rt-stage",0.7,0.4],["wait",600],["shot","shots/hover.png"]]'
27
+ node scripts/drive.mjs page.html --actions steps.json --height 1300
28
+ node scripts/drive.mjs page.html --actions steps.json --width 390
29
+ node scripts/drive.mjs page-light.html --actions steps.json --reduced
30
+ node scripts/drive.mjs page.html --actions steps.json --no-webgl
31
+ node scripts/drive.mjs page-light.html --actions steps.json --preload slow.js
32
+ ```
33
+ Options are `--width` (1440), `--height` (1100), `--scale` (1), `--reduced`, `--theme dark|light`, `--no-webgl` (every `getContext("webgl")` or `"webgl2"` returns `null`, to check the SVG fallback), `--preload file.js` (a script that runs before the page's own scripts: counters, stubs, hooks, or `window.__driveSpeed=0.04` to run slowly from the first frame) and `--actions file.json`. Selectors default to the first `.iso-plate`, and fractions run 0..1 across the element.
34
+
35
+ `--theme` (in both scripts) only emulates `prefers-color-scheme`. Kit pages choose their theme when they are built (`data-theme` on `<body>`) and ignore that media query, so to check the light theme, build with `--light` and drive the `-light.html` file.
36
+
37
+ | Action | What it does |
38
+ |---|---|
39
+ | `["scroll", sel]` | Scrolls the element into the middle of the viewport. |
40
+ | `["wait", ms]` | Waits. |
41
+ | `["move", sel, fx, fy]` | Glides the mouse there in 10 steps, firing `pointermove`. |
42
+ | `["click", sel, fx?, fy?]` | Clicks, at the centre by default. |
43
+ | `["drag", sel, fx0, fy0, fx1, fy1, steps?]` | Presses at one point, glides to the other in `steps` (20) moves a frame apart, releases. These are real input events (pointer id 1), so a handler that calls `setPointerCapture` works. Test drag-to-turn with it: `PointerEvent`s dispatched from `eval` have no active pointer, and `setPointerCapture` throws `NotFoundError` and aborts the handler. |
44
+ | `["key", key, sel?, code?]` | Focuses `sel` (default `[tabindex="0"]`) and presses a key: `"i"`, `"Escape"`, `"ArrowRight"`, `" "`, `"7"`. Digits send `DigitN`, letters `KeyX` and punctuation its proper code (`Space`, `Minus`, `Comma`…). Pass `code` for keys that share a name: `["key", "Shift", null, "ShiftRight"]`, `["key", "Enter", null, "NumpadEnter"]`. |
45
+ | `["slow", factor]` | Runs the page's `requestAnimationFrame` timestamps and `performance.now` at `factor` × real time: `["slow", 0.05]` is 20× slow motion, so a sheet catches a 100 ms event in many frames. `["slow", 1]` restores real time. |
46
+ | `["until", js, timeoutMs?]` | Polls a JS expression every 25 ms until it is truthy (default 10 s), and prints a line if it times out. |
47
+ | `["eval", js]` | Runs JS in the page and prints the result. Promises are awaited. |
48
+ | `["shot", out, sel?, scale?, [x,y,w,h]?]` | Screenshots an element, or a region of it in CSS px. |
49
+ | `["sheet", out, every, count, sel?, scale?, [x,y,w,h]?, columns?]` | Takes `count` frames `every` ms apart and tiles them into one numbered PNG. |
50
+ | `["turn", value, group?]` | 3D figures: sets `window.__isoTurn` (a number sets the first turn group, `group` names another, an object sets several) and waits two frames. |
51
+ | `["orbit", out, from, to, step, sel?, scale?, [x,y,w,h]?, columns?]` | 3D figures: a contact sheet across angles of the first turn group, each frame labelled. `["orbit", "swap.png", 129.7, 130.2, 0.1, null, 6, [x, y, w, h]]` is a 6× close-up of one order change. |
52
+ | `["lines", out?, sel?, { tolerance }?]` | The line-end check below, in whatever pose the actions before it left the figure (apart, mid-stroke). `out` writes the card with every floating end ringed. A failure makes the run exit 1. |
53
+
54
+ Traps:
55
+ - **Shots are correct after `scroll`**, so you can scroll to a figure lower on a page. A viewport tall enough for the whole card (`--height 1300`) is still the simplest: the figure is visible at scroll 0, and its `IntersectionObserver` starts it.
56
+ - **Frame spacing in a sheet is `every` plus the capture time** (60–200 ms, more on heavy WebGL pages, where capture also stalls animation frames). For transitions faster than that, put `["slow", 0.05]` before the sheet and `["slow", 1]` after it. `wait`, `every` and the page's `setTimeout` timers stay in real time, so at 0.05 a sheet every 100 ms is 5 ms of page time apart, and an idle tour's resume timer still fires on the wall clock: a key press holds it off for only about 3.6 s of real time, so press a harmless key again before each slowed sheet. An event that starts on load needs the page slow from its first frame: `--preload` a file with `window.__driveSpeed=0.04`.
57
+ - **Slow mode doesn't slow timers.** `setTimeout` runs on the wall clock, so during a long slowed capture an idle tour's resume timer (the Raptor's 3.6 s `quietTour`) can restart the tour and close the engine mid-sheet. Before every slowed sheet stop the tour and wait for rest: preload a stub for that timer (`const st = window.setTimeout; window.setTimeout = (f, d, ...a) => (d === 3600 ? 0 : st(f, d, ...a));`), press `Escape`, `["until", "document.querySelector('[data-readout]').textContent.includes('cold')"]`, and only then the key that starts the transition. A sheet labelled "opening" that starts from a tour that has already opened films a close.
58
+ - **Wait for a phase, don't guess.** `["until", "document.querySelector('[data-readout]').textContent.includes('main stage')", 8000]` returns as soon as the readout reaches the phase.
59
+ - **A `null` selector is the default.** In every action a missing or `null` selector means the first `.iso-plate` (for `key`, the focusable `[tabindex="0"]`), so `["sheet", "s.png", 200, 16, null, 0.5]` films the card.
60
+ - **Long eval strings break inline JSON quoting.** Put anything with `eval` or `until` in `--actions steps.json`.
61
+ - **Pages inline their live code as `<script type="module">`,** so top-level variables aren't visible to `eval`. Read state through the DOM instead: readout text, `aria-valuetext`, `data-*` attributes. To count frames or read a value per frame, install a hook with `--preload` (it runs before the page, so it can wrap `requestAnimationFrame` or `HTMLCanvasElement.prototype.getContext`).
62
+
63
+ ## Line ends
64
+ The audit sees recorded shapes, never line markup, so it can't see a seam, rule, rib or tick that stops in the middle of a face. `scripts/lines.mjs` checks the drawing itself, in headless Chrome:
65
+
66
+ ```sh
67
+ node scripts/lines.mjs page.html --out shots/lines.png
68
+ node scripts/lines.mjs page-light.html --out shots/lines-light.png --reduced
69
+ node scripts/drive.mjs page.html '[["key"," "],["wait",2000],["lines","shots/lines-apart.png"]]'
70
+ ```
71
+ ```
72
+ line ends: 3 of 1148 ends on 295 lines float
73
+ iso-line faint in it k -0.080 end 7.25 px from anything at card 507.4,407.2 length 75.7 d M392.1 381.7L507.4 407.2…
74
+ ```
75
+ Every open subpath of every `.iso-line` in the card (seams, grooves, ribs from `fadedSvg`, rule lines, ticks, needles, pointers, wires) must end within 1 viewBox px of another stroke, a dot or an outline, or under a fill painted after it. A short straight mark (up to 7 px: a tick) needs one end rooted within 2 px; both ends floating fails. The kit's own outlines, creases, bevels and tube edges aren't checked, and neither are ends that a `fadeLineSvg` ramp fades out. `lineSvg(d, { free: true })` (or `"start"`, `"end"`) marks an end that is meant to stop: rule lines that stand for printed text, a needle's tail, a pointer's tip. Use it for nothing else. It prints each floating end in card px, ready for `capture.mjs --zoom`, with the start of its `d` so you can find it in the build; `--out` rings them on a 2× shot of the card. Run it on both themes and in every pose the figure rests in, and fix every line until it prints 0:
76
+
77
+ | You see | Fix |
78
+ |---|---|
79
+ | A seam or weld line on a box inset from the corners | `k.sideSeam(plan, z, P, steps)` as the slab's `inner`: it wraps the corners and ends on the silhouette (`craft.md`, section 3). |
80
+ | A stiffener or panel line on one face, inset at both ends | Run it edge to edge: from the face's bottom to its top, or from end to end. |
81
+ | Ribs that stop short of their seams | `ribsOf(…, { seams: true })`, or `s0`, `s1` on the band's ends. |
82
+ | Ticks floating on a bare plate or hanging below an edge | Put them on a ruler strip, rooted on its edge (`topTicks` `edge`, `sideTicks` `top` on the face's top edge). |
83
+ | Vent or grille lines | Closed slots: a thin rounded outline, not a line. |
84
+ | A line that runs under a part and stops just past it | Draw it before the part, full length, and let the part hide it. |
85
+ | Rule lines on a label plate | `free: true`. They stand for text. |
86
+
87
+ ## Audit
88
+ Screenshots catch what you think to look at. A figure with pipes, cables and round parts also needs the geometry checked, because a cable through a duct or a part drawn over the thing in front of it is invisible at 1× and obvious at 4×. Wire `kit/audit.mjs` into the build (`kit.md`, section 12): helpers put into one `A.recorder(P, { order, spring })`, the build calls `A.settle(R, P)` and ends with `A.auditOrExit(R, P)`, then run:
89
+
90
+ ```sh
91
+ node examples/raptor-engine/build.mjs --audit
92
+ ```
93
+ ```
94
+ clearance: 0 problems
95
+ clearance apart: 0 problems
96
+ terminals: 0 ends not on a mount
97
+ loose ends: 0
98
+ screen folds: 0
99
+ support: 0 unsupported solids · 0 hanging tubes · 0 sunk pairs
100
+ depth order: 0 problems
101
+ through in mid-flight: 0 pairs
102
+ self-overlap: 0 chunks whose outline crosses itself on screen
103
+ wobble: 0 smoothed profiles whose slope turns between its ends
104
+ saddles off their host apart: 0
105
+ stand-in shapes: 0 items whose recorded shape covers too little of what it draws
106
+ crowding: 0 pairs where a bend or end touches another line on screen
107
+ free ends apart (marked free, not failing): 35
108
+ audit · 80 tubes · 267 solids · 478 items · 1119 depth states · 1707702 overlaps · 4292 order rules · 151.9 s
109
+ audit passed
110
+ ```
111
+ It prints `audit passed` or `audit failed: n problems` and exits non-zero on any failure. Every line counts, including what happens in mid-flight and `items with no shape`, except two lists that are there to be read: `in passing` and `free ends apart`. Depth order and pass-through (tube against everything, and solid against solid) are both checked on the same replayed frames: every frame of the springs opening and closing until they settle, every frame of a reversal 8 ms either side of each stagger boundary (`reversalsOf`: 0.027, 0.043, 0.062, 0.078 … 0.393 s on the Raptor's 35 ms stagger), and the prefix states (`prefixStates`). Depth uses every fourth of those frames. How to read and fix each line:
112
+
113
+ | Line | Meaning | Fix |
114
+ |---|---|---|
115
+ | `through A × B clearance -2.4` | Tube A intersects B over that arc-length span. | Reroute A round B with about 1.5 radii to spare, or move B. Only the shape A's end rim sits on is excused, and only within a radius or so of that end. |
116
+ | `close A × B clearance 0.2 < 0.6` | A passes B with less than half its radius to spare. | Nudge the route; widen harness lane spacing. |
117
+ | `clearance apart: through share 1 …` | In the apart state a tube passes through something. | Change that assembly's explode vector. |
118
+ | `terminals: A end 1.40 from B (rim -1.1…1.3)` | A's end, or the rim of its end flange, doesn't sit on its mount: it floats, it is tilted, or the flange is wider than the boss under it. | End the route exactly on the surface, along the surface normal; make the boss at least as wide as the flange; on a curved body seat the flange on a saddle pad. A cable never ends on a clamp. |
119
+ | `loose ends: share 1 A end touches nothing that moves with it` | Apart, the end hangs in the air: nothing of another owner moves with it (its own flange or shell doesn't count). | Give it a mount that moves with it, or, if it is meant to come off (a flanged duct, a connector on another assembly), mark it `free` so it is listed under "free ends" as a choice. |
120
+ | `self-overlap: A:3 its outline folds back on screen` | One chunk's outline crosses itself: the line turns on a screen radius under its own half-width (a leg pointing at the viewer that turns near a port). | Leave the port through an elbow ball (the Raptor's `elbow`), give the turn more straight leg, or bend in a plane that faces the viewer (the Raptor's `sideOn`, `craft.md`, section 8). |
121
+ | `wobble: A dr/ds turns at s = …` | A profile smoothed from typed knots has a slope that plateaus and reverses, so its tone bands staircase. | Give it an analytic `r(s)` and pass `smooth: { slope }`. |
122
+ | `saddles off their host apart: A (piece) is cut to B (host piece)` | A saddle pad flies off with another assembly, showing its cut base. | Give the pad to the host's assembly; the moving pipe leaves its flat face. |
123
+ | `solid state A × B sunk 1.81 deep` | Two solids of different assemblies pass through each other in that frame. | As for a tube: fix the order or the vectors, or open the gap under an overhang. |
124
+ | `screen folds: A turns 150° on screen for 78° in 3D` | The route doubles back on screen and reads as a cusp. | Route the leg before the turn sideways on screen. |
125
+ | `screen folds: A turns 110° on screen round a 0.4 px bend` | A visible bend pinches to a point: its fillet is seen edge-on, so the two legs meet in a V. | Turn inside a fitting (a ball tee, the line leaving at 45°), bend in a plane that faces the viewer, or move the port so its normal lies across the screen. |
126
+ | `through in mid-flight: apart 0.233 s A × B clearance -1.6` | In that replayed frame A passes through B. | Fix the order or the vectors (`motion.md`, "Order the stagger"): the part outside leaves first, a vector that points into another assembly's face needs that assembly to leave earlier, threaded parts share a spring. |
127
+ | `crowding: A bend × B outlines overlap 0.05 px` | A's bend or end lands on B's outline on screen and reads as a joint or a line threaded through B. | Reroute so the meeting is a clear crossing of two straights, or leave a visible gap. Excuse a pair (`allow`) only after looking at it at 8×. |
128
+ | `crowding: A end × B (rod)` | A's end or bend lands on the outline of a slender solid: a tie rod, piston rod, post, rail or handle (any solid longer than 4× its radius or half-width). It reads as the rod plugged into A's fitting. | Approach the port across the rod, or turn the elbow so the last leg lies across the screen. |
129
+ | `stand-in shapes: A its shapes cover 4% of its drawing` | The shapes recorded for A cover much less of the screen than A draws, so the audit checks a sliver and says nothing about the rest. | Record the true shape of what you draw (`A.slide` for a rod that slides out of a gland). Excuse an item (`cover: [name]`) only when its drawing really reaches past its body, and only after looking at it. |
130
+ | `crowding: C fitting × B` | A clamp lands within 2 px of another line and reads as that line growing out of it. | Change the explode vector or the clamp's angle. |
131
+ | `support: loose A` / `hanging A` / `sunk A × B 1.2 deep` | A solid, or a rigid tube, touches nothing that leads to the stand, or is buried in B. | Add the bracket, stud or plate that holds it; for a sunk part fix the contact (rim on surface, pad cut by its host marked `seated`; `seated` only excuses the host in its `cut`). |
132
+ | `order A is in front of B but drawn first` | A's surface is nearer wherever they overlap, but B paints over it, at rest or in a mid-flight frame. | `orderKeys` normally fixes these; one left over means a rule was dropped from a loop, so look for the interlocked pair in that loop. |
133
+ | `cycle A … (n the other way)` | A and B are each in front of the other somewhere: they interpenetrate on screen. | Change the geometry: gap the pipe at its collar, split the band at the ring, end the rod on the ball, make the volute tangent, put the valve on a straight, keep a pad off its host's silhouette. |
134
+ | `in passing` | Interlocks, or can't be ordered by any `k + lift · share`, in a uniform state only (all assemblies at one share, which the page never shows). | Not a failure. The same thing in a replayed frame is a `depth order` problem. |
135
+ | `items with no shape` | Something is drawn without a recorded shape, so nothing checks it. | Record it in the helper that draws it. |
136
+
137
+ A zero audit is necessary, not sufficient: it can't see a seam, a staircase or a texture that stops, and it only checks what you recorded. A stand-in shape that silences it is a bug, which is why it fails one. Run the line check, then look.
138
+
139
+ ## Orbit (3D figures)
140
+ A figure with turning groups (`references/3d.md`) adds the orbit audit and the browser checks. Run them on the light page and the dark page.
141
+ ```sh
142
+ node build.mjs --light --audit > audit.txt 2>&1 & # the full run, in the background: ends "turn audit passed"
143
+ node build.mjs --light --audit --quick # ≈ 10× fewer states while iterating: ends "turn audit quick"
144
+ node build.mjs --light --audit --quick --only=order,continuity # a subset: the rest print "skipped", ends "turn audit partial"
145
+ node build.mjs --light --verify # a page with window.__isoVerify (recorded shapes) for --order
146
+ node ../../scripts/turn-check.mjs build.mjs --fidelity
147
+ node ../../scripts/turn-check.mjs page-verify-light.html --order --step 1
148
+ node ../../scripts/turn-check.mjs page-verify-light.html --order --states clip.json # hinges and clips: --step turns the first turn group only
149
+ node ../../scripts/turn-check.mjs page-light.html --pops --no-webgl --states tilt-path.json # one hinge as a continuous path at 0.02°
150
+ node ../../scripts/turn-check.mjs page-light.html --pops --no-webgl --step 1 --out shots/pops
151
+ node ../../scripts/turn-check.mjs page-light.html --lines
152
+ node ../../scripts/turn-check.mjs page-light.html --perf --width 1440
153
+ node ../../scripts/turn-check.mjs page-light.html --perf --width 390 --throttle 4
154
+ node ../../scripts/drive.mjs page-light.html '[["orbit", "shots/orbit.png", 0, 345, 15]]'
155
+ ```
156
+ **Time.** A 28-part figure with one turn, one hinge and one clip took 85 s quick and 17 minutes full, past a 10-minute shell timeout: run the full audit in the background. Quick runs undersample (`continuity` checks 100 halving candidates instead of 800, and the order scan a tenth of the states), so they guide, and only the full run's `turn audit passed` counts. A run with `--only` ends `turn audit partial`, never `passed`.
157
+
158
+ **States.** The audit sweeps each turn group at 0.3° (order) and 0.05° (continuity), plus 2000 random states and every order-change event ± 0.001°. Motion clips (`clips: [{ name, at(t), from, to, frame }]`) are walked frame by frame: a gesture, a grasp, a break-apart. Free and hinge groups are proved only on the states you give, so give every clip, at several turn angles.
159
+
160
+ **Lines.** `separation`, `unseparated`, `cycles`, `forced`, `swaps`, `layers`, `rigid`, `swept`, `posed`, `folds`, `self`, `crowding`, `endon`, `coverage`, `depth`, `continuity`. `3d.md`, section 13, has each line's failure and fix. A planted fault fails each line: a sunk pad (`separation`), a pinwheel of three tilted sticks (`cycles`), a fibre bent in a vertical plane (`folds`), a tube at 30° (`endon`), a rib row that stops (`continuity`).
161
+
162
+ **Browser checks** (each runs its control first; a control that doesn't fail means the check is broken):
163
+ - `--order`: the recorded shapes in a WebGL z-buffer against the DOM's fills: 0 live pairs with 4+ interior pixels at every angle and clip state.
164
+ - `--pops`: first difference at every degree (θ → θ + 0.02°) and second difference at 0.1° over a full turn: 0 surviving regions, 0 cores of 6+ px. Read the 1–5 px cores it lists.
165
+ - `--lines`: 0 floating ends at 24 angles.
166
+ - `--perf`: the budgets in `3d.md`, section 15, at 1440 and at 390 with 4× CPU.
167
+
168
+ **Orbit sheets.** 0–345° at 15°, then 0.1° steps across the three largest order changes (the audit lists them under "order changes"), then every clip slowed (`["slow", 0.05]`). Read them for spider angles (a hand that reads as legs from some side), parts that vanish behind others, and lines that end on another part's outline.
169
+
170
+ ### Checklist for 3D figures
171
+ - [ ] `turn audit passed` with the full state set (not `--quick`, not `--only`), clips included.
172
+ - [ ] `--order` on `--states` for every hinge and clip, `--pops` on a 0.02° path for every hinge (`3d.md`, section 14).
173
+ - [ ] `turn-check` order, pops and lines at 0, fidelity passed, every control failing as designed.
174
+ - [ ] Orbit sheets read at 15°; 8× crops of every knuckle, crossing and texture end at 8 angles, light and dark.
175
+ - [ ] Materials read as metal on the light page: highlight bands move with the turn, nothing pops at a tone crossfade.
176
+ - [ ] Every shader anchored to its group follows it at 0.1° steps; world effects don't turn; ink stops at occluding strokes (8× crop where a cover mask meets a part).
177
+ - [ ] Budgets measured at 1440 and 390 × 4, with the half-rate lever if a dense figure needs it.
178
+
179
+ ## Close-ups
180
+ The owner found the Raptor's faults on a close-up of the light theme: cables over the main duct, a stepped bell, bolt rows that stopped dead. Shoot that close-up yourself, everywhere, before anyone else does.
181
+
182
+ 1. **Build both themes** (`node build.mjs` and `node build.mjs --light`) and list the junctions: every flange, tee, elbow, ball joint, clamp, valve, bellows, band, saddle pad and crossing, every curved body (bells, throats, domes, cones, valve bulges, balls), every band joint of a body cut with `bandsOf`, and every place a texture ends. On flat parts too: every seam, weld, stiffener and trim line on a box face (does it reach both corners?), every row of ticks or marks on a plate (is it on a ruler strip?), every rib row's two ends along its axis, every textured hose at a chunk joint and at its ferrules, every rod that slides (at rest, mid-stroke and at full stroke), and every window or screen with text or shader content in it.
183
+ 2. **Shoot each area at 4× in both themes, assembled.** `--zoom x,y,w,h` takes a region in card px and scales it by `--zoom-scale` (4):
184
+ ```sh
185
+ node scripts/capture.mjs page.html --out shots/joint-dark.png --zoom 200,80,220,170
186
+ node scripts/capture.mjs page-light.html --out shots/joint-light.png --zoom 200,80,220,170
187
+ ```
188
+ 3. **And apart**, if the figure comes apart. Drive it there, wait for rest, then shoot a region at scale 4. The Raptor toggles apart on Space and its readout starts `apart ·` only once every spring has arrived, so `apart.json` is
189
+ ```json
190
+ [["key", " "], ["until", "document.querySelector('[data-readout]').textContent.startsWith('apart')"], ["wait", 400],
191
+ ["shot", "shots/apart-joint.png", null, 4, [200, 80, 220, 170]]]
192
+ ```
193
+ run with `node scripts/drive.mjs page.html --actions apart.json`, then again on `page-light.html`.
194
+ 4. **Look at each PNG for these faults, in this order:** a line that runs through or into something; a crossing whose nearer line is drawn behind; a bend, end or clamp sitting on another line's outline; a flange standing off its port or sunk into it; tone blocks stepping across a curved surface, scallops or a pale wedge; a band ending on a straight cut; a row of ribs, dots or rule lines stopping in the middle of a surface; a hairline across a tube's shine or shade at a chunk joint; a seam notching the outline; a seam, stiffener or rib that stops short of the edge it should reach; a short mark on a top that touches nothing; a hose texture with straight hard sides, or one that restarts or is cut at a chunk joint; a rod whose outline breaks while it moves; text that crosses its window; shader content that spills past an opening's rim. Compare with the previous round's shot of the same region.
195
+ 5. **Shaders:** the same regions during the effect, plus contact sheets slowed with `["slow", 0.05]` (`webgl.md`, section 9).
196
+
197
+ **Detail pass.** Last, sweep at 4× in a grid of crops (the subject and mechanism for a Figure, the whole drawing for a Hero or an Epic; `SKILL.md`, workflow step 1) and look for any face, corner or stretch of base with nothing true on it. Every figure is highly detailed (`SKILL.md`, "Every creation is highly detailed"): add the real parts that belong there (screws, seams, bevels, label plates, scales, the brackets that hold things), never decoration that belongs to no part.
198
+
199
+ ## Light-theme shaders
200
+ Light is the shipping theme, and on paper a glow is ink. Every light conversion so far failed review first on something a picture of one frame doesn't show: ink that rose while the light fell, a pale flash before the glow, a salmon stage, a fringe beside a stroke. So measure the ink as well as looking at it (`webgl.md`, section 8).
201
+
202
+ **Ink per frame.** Save this as `ink.js` and pass it with `--preload`. For every frame the page draws, it logs the mean ink one canvas region lays on white, `255 − lum(rgb + 255 − a)`, read back in the same task as the draw:
203
+ ```js
204
+ const get = HTMLCanvasElement.prototype.getContext;
205
+ HTMLCanvasElement.prototype.getContext = function (kind, ...rest) {
206
+ const gl = get.call(this, kind, ...rest);
207
+ if (gl && String(kind).startsWith("webgl") && !this.__gl) {
208
+ this.__gl = gl;
209
+ for (const name of ["drawArrays", "clear"]) {
210
+ const call = gl[name].bind(gl);
211
+ gl[name] = (...args) => { gl.__drew = true; return call(...args); };
212
+ }
213
+ }
214
+ return gl;
215
+ };
216
+ const raf = window.requestAnimationFrame.bind(window);
217
+ window.__ink = [];
218
+ window.requestAnimationFrame = (callback) => raf((now) => {
219
+ callback(now);
220
+ const canvas = document.querySelector(window.__inkCanvas || "canvas");
221
+ const gl = canvas && canvas.__gl;
222
+ if (!gl || !gl.__drew) return;
223
+ gl.__drew = false;
224
+ const [x, y, w, h] = window.__inkBox || [0, 0, canvas.width, canvas.height];
225
+ const px = new Uint8Array(w * h * 4);
226
+ gl.readPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
227
+ let ink = 0;
228
+ for (let i = 0; i < px.length; i += 4) {
229
+ const bare = 255 - px[i + 3];
230
+ ink += 255 - (0.2126 * (px[i] + bare) + 0.7152 * (px[i + 1] + bare) + 0.0722 * (px[i + 2] + bare));
231
+ }
232
+ window.__ink.push(Math.round((ink / (w * h)) * 100) / 100);
233
+ });
234
+ ```
235
+ - **Choose what to measure.** Before the transition, set `window.__inkCanvas` (a selector) and `window.__inkBox` (`[x, y, w, h]` in device px of the canvas, measured from its bottom-left; scale CSS px by `canvas.width / canvas.clientWidth`) with an `eval`. Read the rows afterwards with `["eval", "window.__ink.join(' ')"]`.
236
+ - **Do it once per region that matters:** a cavity, the core, an area only the fallback covers. Run it slowed, across first light (`--preload` a second file with `window.__driveSpeed=0.04`), every phase change and the fade-out.
237
+ - **What to look for.** Ink follows the physical quantity monotonically: up as the light comes in, down as it goes. There is no hump at a hand-off and no dip at first light.
238
+ - **What it caught.** The afterburner's cavity peaked at 3600 on the cut before its fix; afterwards it fell monotonically from 1894 to 155. Those rows are in that figure's own units, a sum over its window region rather than this hook's mean. On the ripple tank's light page the hook reads 14.2 at rest and falls as the lamp comes in, because that page's light brightens the paper.
239
+
240
+ **Colour along the ramp.** For anything that heats or cools, `eval` a pixel read at the centre and the rim at several temperatures (or several temperatures' worth of a slowed run) and write them down. The order must be dull red → orange → yellow → near-white, with the rim cooler than the centre and no salmon or grey stage on the way.
241
+
242
+ **Cross-sections.** Read one row of ink across a flame, a jet or a stream. It peaks on the axis and falls to translucent edges. An edge-heavy profile is a hollow, outlined flame.
243
+
244
+ **Strokes at 8–16×.** Crop every place where ink meets an SVG stroke (cut faces, rims, mouths, creases, a canvas edge with a body entering) at 8–16× in light. Look for a pale gap between the ink and the line, ink over the line, or a stroke the ink cleared.
245
+
246
+ **Both paths, and nested.** The dark build should match its baseline, because every light change is gated on `uLight`. Render a dark plate on a light page and a light plate on its own, each with and without WebGL: a `:not([data-theme="dark"])` rule can reach through the page into a dark plate.
247
+
248
+ ## The review checklist
249
+ **It is an object**
250
+ - [ ] At a glance it reads as one real, well-made thing, and you can name it.
251
+ - [ ] Its mechanism maps to the concept: what moves and what that means can be said in one sentence.
252
+ - [ ] It isn't a stack of abstract plates and isn't one plain slab.
253
+
254
+ **Parts and volume**
255
+ - [ ] 30–60 solids for an inline figure; a hero subject that really has more parts can go far past that (the Raptor has about 270). Every part rests on another and has a job.
256
+ - [ ] Sides shaded and tops lighter; bevels on the main slabs; a halo under the base and under lifted parts.
257
+ - [ ] Small details belong to parts: screws at the corners of plates, grooves, rulers, knurls, label plates.
258
+
259
+ **Lines**
260
+ - [ ] At 4×, every line ends on a face, another line or a terminal dot. Nothing stops in the air.
261
+ - [ ] No leader lines, balloons, dimension arrows or floating labels; at most one engraved label.
262
+ - [ ] Painter's order is right everywhere: nothing behind shows through, springs and frames are split.
263
+ - [ ] **Audit zero.** With pipes, cables or round parts in the figure, `node build.mjs --audit` ends `audit passed`: zero on every failing line, assembled, apart and in every frame of the replayed explode (opening, closing and reversing). No interlock was hidden with a key bias, and every excused pair (`allow`, `crowd`) has been looked at at 8×.
264
+ - [ ] No pipe or cable passes through another pipe, cable or solid, and every crossing is drawn in depth order: the nearer line on top, crossing on straights, well apart in depth.
265
+ - [ ] No line runs across the subject's face where it could run beside the structure; no bend pinches to a V on screen; no bend, end or clamp lands on another line's outline, assembled or apart.
266
+ - [ ] Every flange sits on its port (on a saddle pad if the port is curved); every collar, clamp, band, bellows and valve gaps the pipe it wraps; every valve and bellows sits on a straight; no route doubles back on screen; every end, apart too, ends on a flange, coupling, connector or terminal dot.
267
+ - [ ] **Junction close-ups.** Every junction area shot at 4× in both themes, assembled and apart (see "Close-ups"), and each PNG read.
268
+ - [ ] **No staircase.** Every curved surface is smooth-shaded at 4×: no stepped tone blocks, no scalloped terraces, no pale wedge, no band that ends on a straight cut, no kink at a band joint; waisted bodies follow their true outline; seams meet the outline without a nick.
269
+ - [ ] **Line ends zero.** `node scripts/lines.mjs` prints `line ends: 0` on both themes and in every resting pose; every `free` end is a rule line standing for text, a needle tail or a pointer tip.
270
+ - [ ] **Shapes are true.** No stand-in shapes: what is recorded is what is drawn, including rods that slide.
271
+ - [ ] **No abrupt texture ends.** Every row of ribs, bolt dots, rule lines or grooves ends on a real edge or fades out; none stops at full strength in the middle of a surface, flat faces included. Seams on boxes wrap their corners, rib rows end on their seams, ticks sit on a ruler strip, and a texture on a hose follows its cross-section and runs through every chunk joint. Pipe stripes run onto their end caps and show no hairline at any chunk joint, also on a strongly curved tapered tube (a volute).
272
+
273
+ **Tone and colour**
274
+ - [ ] Base `lo`, structure `mid`, subject `hi`, exactly one `lit` accent.
275
+ - [ ] Monochrome unless colour is the subject.
276
+
277
+ **Card and words**
278
+ - [ ] `Fig n` / title / hint / live readout are in the corners, with legend keys and a caption under the card.
279
+ - [ ] Every number is true and comes from the model. The readout changes as things move, and its words and numbers come from the same instant: a phase word from the model next to a number from a lagging gauge reads `relief 85 bar` before the valve has cracked.
280
+ - [ ] Text the machine displays fits its window at its widest string and is clipped to it.
281
+
282
+ **Motion and access**
283
+ - [ ] Eased, calm, no snapping; an idle tour; pauses on interaction.
284
+ - [ ] Stops offscreen; still under `--reduced`; no console errors (including React key warnings).
285
+ - [ ] Keyboard works, the focus ring shows, and `aria-valuetext` reads the state in words.
286
+
287
+ **Shaders (if the figure has one; `references/webgl.md`)**
288
+ - [ ] A contact sheet across every transition (start, each phase change, steady, stop), slowed with `["slow", 0.05]` (or `--preload` with `window.__driveSpeed=0.04` for an effect that starts on load) so every frame of each crossfade shows: no sudden colour switch, no one-frame flash, nothing cut out, and brightness never rises during a fade-out.
289
+ - [ ] 2× and 4× crops where the effect meets the SVG: no seams, hard edges, banding, dark collars or double lines; things on a plane register with that plane's lines.
290
+ - [ ] The canvas's outer rows and columns read alpha 0 in every phase.
291
+ - [ ] Without WebGL (`--no-webgl`) the SVG alone still reads; under reduced motion you get one still, representative frame; the loop stops when idle and offscreen.
292
+ - [ ] **Light theme** (`webgl.md`, section 8; "Light-theme shaders" above):
293
+ - Glows read through saturation, with a saturated body, a pale core and a coloured rim, never as dark or brown ink.
294
+ - What the light falls on is tinted, and the cavity that holds the light is a darker physical ground.
295
+ - Hot bodies run in blackbody order with no salmon stage.
296
+ - Smoke and steam are light grey.
297
+ - There are no halos in empty air.
298
+ - Ink per frame follows the light monotonically through first light, every hand-off and the fade-out.
299
+ - Ink stops at each stroke's inner edge at 8–16×.
300
+ - The fallback shows the same inks and ground, and hands over to the shader without a dip.
301
+ - Dark is unchanged, and a dark plate on a light page still works.
302
+
303
+ **Composition**
304
+ - [ ] Centred, filling about 55–65% of the card; it doesn't touch the corner labels.
305
+ - [ ] Still readable at 390 px with no sideways scroll, with a part selected and during a burn, not only at rest: the longest readout must not run under the controls.
306
+
307
+ ## Common faults and fixes
308
+ | You see | Fix |
309
+ |---|---|
310
+ | A part shows through something in front of it | It is painted too late. Move it earlier, or split the front part. |
311
+ | An ellipse shows across a post where it meets a face | A full `ring` drawn over a cylinder that continues shows its back half. Use `sideArc` (front half only), or let the cylinder's own outline make the edge. |
312
+ | A post looks pasted onto the block it passes through | Draw it in two segments, below and above, with the block painted between them. |
313
+ | A crease or bevel stops short of a corner | Use `bevel` on `slabOf`/`cylinder`. It already ends on the silhouette. Don't hand-draw edge lines. |
314
+ | Flat, dead sides | You passed `flat`, or the solid has height 0. Give it thickness. |
315
+ | Too sparse | Add structure, not texture: feet, rails with rulers and end stops, a label plate, screws, a knob, a second stage. |
316
+ | Too busy | Drop texture-like repeats and tone grooves down to `faint`; keep one lit accent. |
317
+ | The figure is tiny in the card | Fit with a smaller `pad`, or tighten the points passed to `fitProjection`. |
318
+ | It jumps when a tab regains focus | Clamp `dt` to 1/30 s. |
319
+ | Key warnings only on the big page | See `react.md`: key the roots of element props. |
320
+ | A shader colour jumps at a phase change | Give colour its own slow envelope, and drive it from the physical quantity, not from a phase flag. See `webgl.md`, section 7. |
321
+ | A straight edge in the glow, often at the canvas border | A term isn't zero at a boundary. Window it to zero before every `if`, bound and canvas edge. |
322
+ | A glow runs through the floor or over a column | It is in screen space. Rebuild it in world space with the ray and the ground hit, and give simple parts to the shader as occluders. |
323
+ | A dark collar under a glowing rim | The rim is normal-blended over the shader. Move it to an overlay SVG with `mix-blend-mode: screen`. |
324
+ | Flat bands in soft glows | Add gated triangular dither after `tonemap`. |
325
+ | In light, a glow reads as dark red or brown smoke | It is the dark colour darkened, with alpha set to brightness. Solve the ink from a paper ramp with a saturated body, a pale core and an alpha floor (`webgl.md`, section 8.1). |
326
+ | In light, first light goes pale and then dark, or a fade-out greys or gets heavier | Per-term fades, an alpha-only fade, or a hue that reddens at fixed alpha. Fade the finished ink once, dim energy and alpha together, and take the hue at steady brightness (`webgl.md`, section 8.2). |
327
+ | In light, a pale fringe or white seam along an edge | The ink stops short of the stroke, or clears a crease nobody strokes. End it at the stroke's inner edge (0.3 vb) and draw the crease in ink (`webgl.md`, section 8.6). |
328
+ | A dark plate's fallback vanishes on a light page | A `.iso:not([data-theme="dark"])` rule matched through the page. Put the dark rule last, or key on the plate's own theme (`webgl.md`, section 8.5). |
329
+ | A pipe or cable looks pasted on or cuts through a part | Its pieces are in one layer. Depth-sort each `tubePieces` chunk by `mid`, and use shorter pieces where it weaves. If it really passes through the part, reroute it; `--audit` lists every case. |
330
+ | Cables or sense lines fan across the subject's face and over a main duct | They were routed point to point. Re-route along the structure: up beside a pipe, round the body in clamped lanes, round the side to a far port (`craft.md`, "Choosing waypoints"). |
331
+ | Fixing one crossing's order breaks another, or the apart pose | Keys tuned by hand. Record shapes, let `A.settle` set the keys, and fix whatever it reports as interlocked in the geometry. |
332
+ | A collar, band or clamp hides the pipe's near side, or the pipe paints over the collar's face | The pipe runs through it as one chunk. Gap the pipe at the collar (`gaps`), so a piece ends on each face. |
333
+ | A ring on a body shows its back half over the body | The body is one long band under the ring. Split it at the ring. |
334
+ | A valve or bellows cuts into its pipe at the ends | It sits on a bend. Move it to a straight at least its own length plus both fillet cuts. |
335
+ | Stepped tone blocks on a bell, dome or cone | Facet shading. Mark the curved knots smooth (`[s, r, 1]`, shaded smoothly by default) or pass `smooth: true`; cut a body into bands with `G.bandsOf`. |
336
+ | Tone bands on a throat, cone or puck stall and lurch in scallops | A smoothed profile of hand-typed knots: its slope plateaus at each knot. Write the profile as an analytic `r(s)` and pass its derivative as `smooth: { slope }` (`wobble` lists them). |
337
+ | Hairline seams inside a smooth-shaded part | The tone sheets abut. Use the kit's `solidSvg` with `seamless` paths (smooth lathes and `sphereOf` set it) and `ISO_CSS`. |
338
+ | A row of ribs or bolt dots stops in the middle of a surface | A hard `least` cut-off. Use `fade: [lo, hi]` on `ribsOf` or `dotsOf`. |
339
+ | A pale wedge with straight edges on a smooth-shaded cone or bell | Bare fill between tone pieces: pieces of one tone wound in opposite directions cancel under the nonzero rule, or a band ended without tapering. The kit orients every tone polygon and tapers bands; if you build tone paths yourself, orient them. |
340
+ | A narrowing body looks like a straight cone with a pale strip | It was hulled. Let `lathe` pick `fill: "auto"` (or pass `"surface"`). |
341
+ | A hairline crosses a tube's shine or shade band | Body and stripe cut on the same line at a chunk joint. `tubePieces` runs the stripes past the body; keep that if you change it. |
342
+ | The outline is dashed where seams meet it | Seams drawn after the solid. Pass them as `inner`. |
343
+ | A pipe's stripes stop short of its end | An end without a cap. `tubePieces` caps every end by default; don't pass `caps: false` for an end that shows. |
344
+ | Parts slide along their mount while the figure comes apart | They move with one assembly's vector on another's spring. Give the item that assembly's `ride`. |
345
+ | A long turned body has a kinked outline at band joints | Build it with `G.bandsOf` and draw the sides once with its `sidesOf` (`kit.md`, section 10). |
346
+ | A texture line stops in the middle of a face | End it on a real edge, or draw it with `fadeLineSvg(points, { fade })` so it ramps out. `scripts/lines.mjs` lists every one. |
347
+ | A seam on a box stops short of its corners | `k.sideSeam(plan, z, P, steps)` as `inner`. |
348
+ | A hose braid or corrugation with straight hard sides, or broken at chunk joints | `tubePieces(…, { rings: { pitch, twist, cross: true } })`. |
349
+ | A rod's outline notched while it slides | One rod from `G.rodOut`, clipped at the gland, recorded with `A.slide`; never overlapping copies. |
350
+ | LCD or counter digits cross the window | Fit the type to the window's height and widest string, and clip it. |
351
+ | Stray triangles of outline across a valve or bulge | An old kit joined a broken silhouette; the kit now falls back to the hull when a side's silhouette has gaps. Rebuild with the current `lathe.mjs`. |