@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1

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 (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,807 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * WALKTHROUGH — the ordered "Walk-through" rung registry
5
+ * (ws-experiments-walkthrough Bite 3a; CEO ruling Q-CEO-2 + architect ruling A/B/C;
6
+ * re-authored to addObject form in Bite 4b — architect ruling 3).
7
+ *
8
+ * An ORDERED array of rung recipes surfaced as a "Walk-through" group in STUDIO's
9
+ * experiment dropdown. Each rung loads via the blank-page load-by-eval path exactly
10
+ * like a saved project — the SAME array is the SINGLE authority for BOTH the
11
+ * dropdown group AND the `walkthrough:<id>` lookup (no parallel id→source map, no
12
+ * switch statement — architect condition B).
13
+ *
14
+ * A rung row carries the PATH of its scene, never its text: each scene is ONE sketch
15
+ * file, `lib/frqtl/scenes/<file>.js` — a dual-mode `buildExperiment(exp)` module that
16
+ * holds the whole scene. Its text is read through `loadSceneSource(id | rung, opts)`
17
+ * (./scene-source.js: a fetch in a page, a file read in node; FAIL-LOUD on an unknown id,
18
+ * a non-200 or an empty body). Rungs that share one physics name the SAME file.
19
+ *
20
+ * ── Rung descriptor ──────────────────────────────────────────────────────────
21
+ * id : stable dropdown id (the `walkthrough:<id>` selection value)
22
+ * title : ordered display label
23
+ * prose : one-line declarative description (what it shows, not a journey)
24
+ * source : the frqtl-relative path of the scene file, `'scenes/<file>.js'`
25
+ * (resolved through the router's `scenes/` profile key).
26
+ * renderState : OPTIONAL opaque bag (normalizeSavedRenderState shape). The
27
+ * walkthrough load path ferries it into the SAME `_egptPendingRenderState`
28
+ * seam the `project:` path uses (E2a shim-owned contract) — renderer/
29
+ * seed thread as URL overrides (load-time); render options +
30
+ * camera replay after start. NO new mechanism. Absent → 2D/p5 default.
31
+ * modes : OPTIONAL ordered list of named mode entries — `[{ id, label,
32
+ * renderState? }, …]`. The FIRST entry is the rung's DEFAULT mode.
33
+ * Absent (or length 1) → a single-mode rung: zero UI, byte-identical
34
+ * behavior. A mode entry's `renderState` is a DELTA merged over the
35
+ * rung's base `renderState` by the ONE derivation point,
36
+ * `effectiveRenderState(rung, modeId)` (below) — never merged by hand
37
+ * at a call site. The dropdown SELECTION VALUE stays `walkthrough:<id>`
38
+ * in every mode (a mode is never a second experiment id); STUDIO tracks
39
+ * the active mode in `project.walkthroughMode` and re-runs the ordinary
40
+ * walkthrough load path with the effective bag (a reload — the same
41
+ * pattern as `?engine=`).
42
+ *
43
+ * ── The mode-aware recipe form (S3/W2, CEO-ratified 2026-07-26) ───────────────
44
+ * A multi-mode rung has exactly ONE `source`. A mode selects the RENDERER the host boots
45
+ * (`renderState.renderer`); the scene reads the booted display once, before its world, and
46
+ * makes the matching world (`three` ⇒ exp.createScene, else exp.createCanvas) — its
47
+ * dimensional deltas branch on that. PER-MODE SCENE FILES ARE REJECTED: a second source
48
+ * is the config-mirror anti-pattern this contract exists to kill. Every mode delta
49
+ * must be expressible as DECLARED descriptor fields (emitter `is2D`, Wall/Tunnel
50
+ * `z`/`depth`, ordinary Walls for the z-collimators, camera on the renderState) so
51
+ * a built scene codegens to concrete, mode-PINNED state and survives Restart.
52
+ *
53
+ * ── The recipe form (Bite 4b — addObject, codegen-round-trippable) ────────────
54
+ * Rungs are OBJECT recipes: each body states the WORLD first, in its co-init call
55
+ * (`exp.createCanvas(W, H, setupSphereEmitter3D.worldCfg(SRC))`) and then calls
56
+ * `exp.addObject('SphereEmitter', SRC)` with the SAME bag — a PART never inits the
57
+ * world, the sketch does, once, before any part. Because the emitter is now a real
58
+ * authored object (a NONE-collision visual marker + the host emit tick-fn), a loaded
59
+ * rung is DRAGGABLE / INSPECTABLE and CODEGEN-ROUND-TRIPPABLE: a STUDIO grab/drag →
60
+ * Save re-emits the SAME addObject recipe (SphereEmitter imports setupSphereEmitter3D, so no
61
+ * per-rung script injection is needed).
62
+ *
63
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
64
+ * Zero new physics (canonical-bench SU1) — every rung composes already-goldened
65
+ * engine behaviors via the shared sphere-emitter sugar. Each rung un-degenerates ONE
66
+ * already-present input of the previous (theorist Q-THEO-1: strictly compositional).
67
+ * The addObject builder forwards the opts VERBATIM to setupSphereEmitter3D, so the
68
+ * physics/visual is BYTE-IDENTICAL to the pre-Bite-4b factory-call rungs.
69
+ */
70
+
71
+ 'use strict';
72
+
73
+ // The ONE shim-level renderState normalizer (E2a) — `effectiveRenderState` merges
74
+ // through it so a mode bag can never drift from the saved-project bag shape.
75
+ import { normalizeSavedRenderState, renderRowsFromBag } from '../render-state/index.js';
76
+ // The sun's look — the ONE owner of its render values.
77
+ import * as SunLook from '../render/sun-look.js';
78
+
79
+ // ── Shared render-variant renderState deltas ─────────────────────────────────
80
+ // All ride the SAME filter.<name>->setFilterActive / setRenderOption ControlBind
81
+ // dispatch a saved project's renderState uses (E2a/S6 seam; p5 supplies the filters).
82
+
83
+ // Fountain-pool: hide particles + water shader (Rung 5, Rung 6B).
84
+ var WATER_RENDER_STATE = {
85
+ render: [
86
+ { key: 'renderBaseCanvas', value: false },
87
+ { key: 'filter.water', value: true }
88
+ ]
89
+ };
90
+ // Plasma fusion: particles ON + water + additive bloom on the live field (Rung 6C).
91
+ var PLASMA_RENDER_STATE = {
92
+ render: [
93
+ { key: 'renderBaseCanvas', value: true },
94
+ { key: 'filter.water', value: true },
95
+ { key: 'filter.bloom', value: true }
96
+ ]
97
+ };
98
+ // 3D: the three renderer (the scene's exp.createScene makes the 3D world + perspective camera).
99
+ var THREE_RENDER_STATE = {
100
+ renderer: 'three',
101
+ camera: { orbit: null, preset: 'rooftop' }
102
+ };
103
+ // Sun: 3D + the sun's look. The render VALUES are owned by lib/shim/render/sun-look.js (the CEO's
104
+ // exported FX bag); the camera is this row's own view, verbatim from his exported renderState.
105
+ var SUN_RENDER_STATE = {
106
+ renderer: 'three',
107
+ camera: { orbit: { azimuth: 32.978, elevation: 45, distance: 2873.68 }, preset: null },
108
+ render: renderRowsFromBag(SunLook.SUN_RENDER)
109
+ };
110
+
111
+ /** @type {Array<{id:string, title:string, prose:string, source:string, renderState?:object}>} — `source` is a 'scenes/<file>.js' path */
112
+ export var WALKTHROUGH = [
113
+ {
114
+ id: 'r1-straight-walk',
115
+ title: '1 · Straight Walk',
116
+ prose: 'A collimated line of quanta drifts steadily along +X, each carrying a small ' +
117
+ 'always-armed transverse ripple (λ=16) — the walk in its simplest form.',
118
+ source: 'scenes/straight-walk.js'
119
+ },
120
+ {
121
+ id: 'r2-wave',
122
+ title: '2 · Wave',
123
+ prose: 'The same straight walk, but the transverse coin is given leverage — the ripple ' +
124
+ 'grows into a visible snake as each leaf oscillates with a longer wavelength.',
125
+ source: 'scenes/wave.js'
126
+ },
127
+ {
128
+ id: 'r3-omni-source',
129
+ title: '3 · Omni Source',
130
+ prose: 'Release the walk in every direction at once — a single point source spraying an ' +
131
+ 'even disk of quanta (the sun). Mixed-axis momentum un-degenerates the direction sampler.',
132
+ source: 'scenes/omni-source.js'
133
+ },
134
+ {
135
+ id: 'r4-self-interference',
136
+ title: '4 · Self-Interference',
137
+ prose: 'A single omni source of oppositely-charged quanta launched from a ring — the ' +
138
+ 'overlapping charged walks braid into radial bands of light and dark, an interference ' +
139
+ 'pattern with no wave equation.',
140
+ source: 'scenes/self-interference.js'
141
+ },
142
+ {
143
+ // POSITION IS THE CEO'S CALL (flagged, not decided silently). Placed here because
144
+ // Self-Interference is its closest physics sibling — the same banded fan, reached by
145
+ // building an apparatus instead of by launching an omni source. Using the letter-suffix
146
+ // idiom the 6A/6B/6C rungs already establish, so NO existing rung is renumbered.
147
+ // Moving it (e.g. to the end, after Sun) is a one-line array move.
148
+ id: 'r4b-laser-buildup',
149
+ title: '4B · Laser Build-Up',
150
+ prose: 'A laser, assembled from parts you have already met. A GAIN SOURCE — rung 3\'s omni ' +
151
+ 'emitter, restricted to a forward half-fan — fires into a RESONANT BORE whose walls ' +
152
+ 're-aim every hit forward instead of reflecting it, and the beam leaves through an ' +
153
+ 'OUTPUT COUPLER at the far mouth. The coupler has two independent switches: RECENTER ' +
154
+ 'collapses every exiting quantum onto the bore\'s mid-line, making the mouth a single ' +
155
+ 'point source; PHASE RESET re-locks the ensemble\'s oscillation so equal-radius quanta ' +
156
+ 'share a phase again. Recentering alone gives you the geometry of rung 4; adding phase ' +
157
+ 'gives you its coherence. Sweep the two switches against the wavelength and the bore ' +
158
+ 'length: at long wavelengths the mouth is already nearly in phase and the bands appear ' +
159
+ 'on their own, while at short ones the fan reads smooth until you switch phase on. ' +
160
+ 'Nothing here is a wave equation — it is the same charged walk, aimed and re-timed.',
161
+ source: 'scenes/laser-buildup.js'
162
+ },
163
+ {
164
+ id: 'r5-fountain-pool',
165
+ title: '5 · Fountain Pool',
166
+ prose: 'The SAME self-interference field, worn as water — particles hidden and the water ' +
167
+ 'shader on. The light field and the water field are the same emergent wave, a ' +
168
+ 'different lens (the fractal aha).',
169
+ // SAME physics as Rung 4 (the same scene file) — render delta only.
170
+ source: 'scenes/self-interference.js',
171
+ renderState: WATER_RENDER_STATE
172
+ },
173
+ {
174
+ id: 'r6a-two-source',
175
+ title: '6A · Two Sources',
176
+ prose: 'Two independent self-interference sources, ±32px apart — their overlapping charged ' +
177
+ 'walks form an interference lattice between them: pure density superposition of two ' +
178
+ 'IID populations, interference without a wave equation.',
179
+ source: 'scenes/two-source.js'
180
+ },
181
+ {
182
+ id: 'r6b-two-fountains',
183
+ title: '6B · Two Fountains',
184
+ prose: 'The two-source lattice worn as water — two fountains whose ripple fields overlap ' +
185
+ 'into the same interference web, painted by the water shader with the particles hidden.',
186
+ // SAME physics as Rung 6A (the same scene file) — render delta only.
187
+ source: 'scenes/two-source.js',
188
+ renderState: WATER_RENDER_STATE
189
+ },
190
+ {
191
+ id: 'r6c-plasma-fusion',
192
+ title: '6C · Plasma Fusion',
193
+ prose: 'The two-source field lit as plasma — particles ON, water surface, and an additive ' +
194
+ 'bloom, so the two colliding charged streams read as a fusing plasma core.',
195
+ // SAME physics as Rung 6A (the same scene file) — render delta only.
196
+ source: 'scenes/two-source.js',
197
+ renderState: PLASMA_RENDER_STATE
198
+ },
199
+ {
200
+ id: 'r7-3d',
201
+ title: '7 · 3D',
202
+ prose: 'The omni source released into all three dimensions — a true 3D ball of quanta ' +
203
+ 'rendered with three.js, orbit-able with the mouse or the on-screen camera joysticks.',
204
+ source: 'scenes/omni-3d.js',
205
+ renderState: THREE_RENDER_STATE
206
+ },
207
+ {
208
+ id: 'r8-sun',
209
+ title: '8 · Sun',
210
+ prose: 'The finale — the 3D omni point source dressed as a star: a dense (mass-16, fullness-1) ' +
211
+ 'warm-cool glowing core with volumetric god-rays and a shimmer of heat. The same walk ' +
212
+ 'that drew the pool, now the sun.',
213
+ source: 'scenes/sun.js',
214
+ renderState: SUN_RENDER_STATE
215
+ },
216
+ // ── Collision Lab (CEO 2026-07-23, W1 drive) — the wall-collision primitives,
217
+ // built on the first-class v2 Wall. Scaffolds for interactive tuning (drag /
218
+ // rotate / select / save): the CEO drives them to confirm the base object +
219
+ // control behaviors work bug-free. SU1 — the Walls add zero physics.
220
+ {
221
+ id: 'cl1a-mirror',
222
+ title: '1A · Mirror',
223
+ group: 'Collision Lab',
224
+ prose: 'The first true routing demo: a LASER — a genuinely collimated +x beam (the ' +
225
+ 'tightest emission the emitter supports: pure +x momentum, a single charge, no ' +
226
+ 'wavelength-scale spreading) — fires into a rotatable mirror. The mirror compiles ' +
227
+ 'the real reflection law: a surface tilted to θ deflects the beam by 2θ (standard ' +
228
+ 'optics), so at 45° the thin beam exits straight up. ROTATE the mirror (the ' +
229
+ 'orientation slider, or LOOK-x in 3D) and the thin reflected beam steers live.',
230
+ source: 'scenes/mirror.js'
231
+ },
232
+ {
233
+ id: 'cl1b-absorbing-wall',
234
+ title: '1B · Absorbing Wall',
235
+ group: 'Collision Lab',
236
+ prose: 'A wall that ABSORBS the quanta into its OWN real mass — it fills as it soaks up ' +
237
+ 'arrivals, stops at capacity, and slowly radiates the stored mass back out as ' +
238
+ 'lower-dimension leaves. Mass is exactly conserved (the blackbody absorb/re-emit).',
239
+ source: 'scenes/absorbing-wall.js'
240
+ },
241
+ {
242
+ id: 'cl1c-counting-wall',
243
+ title: '1C · Counting Wall',
244
+ group: 'Collision Lab',
245
+ prose: 'A detector screen — the Detector Screen preset (destroy + count) tallies every ' +
246
+ 'arrival by position. A LIVE in-scene chart (a first-class LiveChart object placed ' +
247
+ 'beside the detector) plots the running y→count histogram as the sim runs — the ' +
248
+ 'chart reads wall.getCollisionHistogram between ticks and accumulates on-screen. ' +
249
+ 'This is the basis for the double-slit fringe readout. The chart panel is a plain ' +
250
+ 'render-only object: drag it, resize it, or point it at any counting wall.',
251
+ source: 'scenes/counting-wall.js'
252
+ },
253
+ {
254
+ id: 'cl1d-mirror-route',
255
+ title: '1D · Mirror Loop (10-mirror {10/3} star)',
256
+ group: 'Collision Lab',
257
+ prose: 'The explanatory centerpiece: a single 2-PARTICLE-WIDE laser threads a CLOSED ' +
258
+ 'TEN-MIRROR {10/3} STAR and LOOPS FOREVER — m1→m2→…→m10→m1→… with no exit. Ten ' +
259
+ 'tall/thin mirrors sit at the vertices of a regular decagon; each mirror\'s tilt is ' +
260
+ 'solved from its incoming/outgoing chords so the reflected centre-line hits the next ' +
261
+ 'mirror exactly, and by the star\'s symmetry the tenth leg feeds straight back into ' +
262
+ 'the first — the polygon closes on itself. Each mirror is a DIFFERENT colour and, on ' +
263
+ 'collision, TAGS the beam with its own colour (its drawn colour = its identity): so a ' +
264
+ 'particle\'s colour history IS its bounce sequence — red after m1, orange after m2, … ' +
265
+ 'magenta after m10, then red again as it re-enters the loop (a pkg-node colour-transition ' +
266
+ 'proof shows the dominant next-colour after m_k is m_{k+1} for all ten, closing the ring). ' +
267
+ 'Reflection off the DRAWN face is the standard-optics 2θ law (the swept-OBB face ' +
268
+ 'collision), ten times over, no wave equation. In 3D (the default mode) every mirror ' +
269
+ 'is a SLAB — a plate with real depth — standing at its decagon vertex with the same ' +
270
+ 'in-plane tilt, and the laser is volumetric: it braids in y AND z, each slab\'s tilted ' +
271
+ 'faces reflect it by the 2θ law (vz passes through) and its near/far z-faces bounce vz, ' +
272
+ 'so no mirror passes the beam; ORBIT the camera (LOOK/drag or the az/el sliders) to see ' +
273
+ 'the ring as plates in space, and z-strays exit between the slabs and die at the ' +
274
+ 'scene\'s z-boundary. Switch the MODE strip to 2D for the flat star — the same ring, ' +
275
+ 'tilts, colours and laser, minus the z-axis (2D≡3D). Because the loop has no sink, the ' +
276
+ 'beam never leaves — an INVISIBLE background Wall (nothing + count hook) spanning the ' +
277
+ 'whole canvas (and, in 3D, the whole volume) counts the live population every tick (its ' +
278
+ 'per-tick collided frames ARE the live quanta), and the emitter\'s DECLARED emission ' +
279
+ 'gate (emissionCap + gateCounter, the blackbody pattern compiled at construction) reads ' +
280
+ 'THAT counter to pause emission at the cap and resume below it, so the population ' +
281
+ 'plateaus and FPS stays healthy while the circuit runs indefinitely — and the gate ' +
282
+ 'survives Restart (it is captured descriptor DATA, not recipe code).',
283
+ source: 'scenes/mirror-route.js',
284
+ // 3D FIRST = the rung's DEFAULT mode (the first declared entry). It opens on the
285
+ // 'table-top' camera preset (three-camera-contract.js cameraPresets) with god-rays on at
286
+ // the sun's settings (SunLook.SUN_GODRAYS, owned by lib/shim/render/sun-look.js), computed
287
+ // from the light particles only (godraySource 'leaf'), so the mirrors shadow the beam and
288
+ // cast no rays.
289
+ // The 2D mode stays one click away on the MODE strip.
290
+ modes: [
291
+ {
292
+ id: '3d', label: '3D',
293
+ renderState: {
294
+ renderer: 'three',
295
+ camera: { orbit: null, preset: 'table-top' },
296
+ render: renderRowsFromBag(Object.assign({}, SunLook.SUN_GODRAYS, { godraySource: 'leaf' }))
297
+ }
298
+ },
299
+ { id: '2d', label: '2D' }
300
+ ]
301
+ },
302
+ // ── Collision Lab · 3D (Z2, CEO UX-pass 2026-07-24) — 1A/1B/1C driven in a 3D scene:
303
+ // a VOLUMETRIC (is2D:false) emitter + a 3D WALL SLAB (depth → a real z-slab, rendered
304
+ // as a BoxGeometry) + the THREE renderer/perspective camera (renderState). The walls'
305
+ // z-slabs CONTAIN z=0 (near<0<far) so the beam/spray — emitted at z=0 — collides. The
306
+ // reflect/absorb/count physics is the SAME as the 2D rungs (SU1); only the render layer
307
+ // and the wall's z-extent change (2D≡3D). Drive with LOOK/orbit (the W0 3D camera).
308
+ {
309
+ id: 'cl1a-mirror-3d',
310
+ title: '1A · Mirror (3D)',
311
+ group: 'Collision Lab · 3D',
312
+ prose: 'The mirror in a 3D scene, FACE-ON: a directional +x beam hits a mirror SLAB ' +
313
+ '(a box you can orbit around) square on its drawn face and bounces STRAIGHT BACK ' +
314
+ '— beam in, recolored beam out along the same line, nothing continuing past the ' +
315
+ 'plate, from every camera angle (an actual mirror). Orbit (LOOK/drag) to see the ' +
316
+ 'retro-reflection in three dimensions; ROTATE the mirror (the orientation slider) ' +
317
+ 'to steer the exit by the same 2θ law as the 2D mirror — identical physics, now a ' +
318
+ 'box in space (2D≡3D).',
319
+ source: 'scenes/mirror-3d.js',
320
+ renderState: THREE_RENDER_STATE
321
+ },
322
+ {
323
+ id: 'cl1b-absorbing-wall-3d',
324
+ title: '1B · Absorbing Wall (3D)',
325
+ group: 'Collision Lab · 3D',
326
+ prose: 'The absorbing wall as a 3D slab: a volumetric (spherical) source sprays quanta in ' +
327
+ 'three dimensions into a wall SLAB that absorbs them into its own real mass, fills ' +
328
+ 'to capacity, and radiates lower-dimension leaves back out — mass exactly conserved. ' +
329
+ 'Orbit the camera to watch the spray soak into the box from every angle (2D≡3D).',
330
+ source: 'scenes/absorbing-wall-3d.js',
331
+ renderState: THREE_RENDER_STATE
332
+ },
333
+ {
334
+ id: 'cl1c-counting-wall-3d',
335
+ title: '1C · Counting Wall (3D)',
336
+ group: 'Collision Lab · 3D',
337
+ prose: 'The detector screen as a 3D slab with a live in-scene chart: a volumetric source ' +
338
+ 'sprays into a Detector Screen SLAB (destroy + count); a first-class LiveChart panel ' +
339
+ '— placed in the scene beside the detector — plots the running arrivals as the sim ' +
340
+ 'runs, textured into the 3D view. Orbit to see the detector box and its chart in ' +
341
+ 'space (the basis for a 3D fringe readout).',
342
+ source: 'scenes/counting-wall-3d.js',
343
+ renderState: THREE_RENDER_STATE
344
+ },
345
+ // ── DSE (CEO 2026-07-24, DSE-rebuild-before-republish; order + titles ruled
346
+ // 2026-07-26) — THE double-slit (on the Tunnel object, the single canonical
347
+ // double-slit) then the photoelectric experiment, both on the full new
348
+ // component set. Letter prefixes are DROPPED: the group label carries the
349
+ // family, and the ids are the stable contract.
350
+ {
351
+ id: 'dse-tunnel-rebuild',
352
+ title: 'Double Slit (Tunnel, symmetric)',
353
+ group: 'DSE',
354
+ prose: 'The double-slit REBUILT ON THE TUNNEL OBJECT at PHYSICAL DSE PROPORTIONS: the ' +
355
+ 'detector sits at the SYMMETRIC distance — emitter-to-front of the slit wall EQUALS ' +
356
+ 'back of the slit wall-to-detector. Each slit channel is a DIRECTED Tunnel: a hit on ' +
357
+ 'a channel wall ejects the quantum back into the bore and re-emits it in a uniformly ' +
358
+ 'random direction within 180 degrees of the corridor axis, so each slit acts as a ' +
359
+ 'semi-circular emitter — the classical picture of what a slit does to a wavefront — ' +
360
+ 'rather than a mirror that would only ever project the beam it was given. The exit ' +
361
+ 'angle is drawn with real trig, so the fan is flat, not diagonal-weighted; the ' +
362
+ 'barriers + screen are v2 Walls (destroy / detector ' +
363
+ 'preset) so the scene survives Restart. The source emits FORWARD over 180 degrees ' +
364
+ '(the hemisphere preset) rather than all the way round, so the burst that used to fly ' +
365
+ 'backward into the catcher now lights the aperture — the same sampler, restricted to ' +
366
+ 'the forward half, aimed at the wall. In 2D a balanced, central-dominant three-lobe ' +
367
+ 'interference pattern builds up. Switch the MODE strip to 3D and the SAME experiment stands up in space — ' +
368
+ 'the barrier becomes a plate with two slots, each channel a real corridor, the source ' +
369
+ 'a volumetric forward half-ball — and the instrument follows: the detector plate paints its OWN hit ' +
370
+ 'map on its faces, exactly where each particle landed, so the fringes read as bands on ' +
371
+ 'the plate itself. Identical physics, one more axis (2D≡3D).',
372
+ source: 'scenes/dse-tunnel-rebuild.js',
373
+ // The spectrally correct ramp (LightColor 'roygbiv', no pink terminus) — the right
374
+ // instrument for a LIGHT experiment, and the one on which this beam's fullness
375
+ // (0.3125) reads as yellow. Declared on the BASE renderState so BOTH modes inherit
376
+ // it (effectiveRenderState CONCATENATES base render rows with the mode's).
377
+ renderState: {
378
+ render: [{ key: 'colorScheme', value: 'roygbiv' }]
379
+ },
380
+ // 3D FIRST = the rung's DEFAULT mode (the first declared entry), because this
381
+ // rung is FIRST CONTACT: STUDIO opens on it, and the thing a newcomer should
382
+ // meet is the experiment standing up in space with a camera they can move.
383
+ // The 2D mode stays one click away on the MODE strip.
384
+ modes: [
385
+ {
386
+ id: '3d', label: '3D',
387
+ // FIRST-CONTACT FRAMING (CEO 2026-07-26): "standing BEHIND the emitter,
388
+ // facing the slit wall slightly rotated, so the interference builds up
389
+ // partially visible and invites you to move the camera."
390
+ //
391
+ // DERIVED FROM THE SCENE, not tuned against a picture. The camera sits at
392
+ // target + d·(cos el·sin az, sin el, cos el·cos az)
393
+ // looking back at `target` (three-provider `_applyOrbit`, world y-DOWN).
394
+ // target (138, 200, 0) — the midpoint of the WORKING LENGTH (emitter
395
+ // x=20 → detector far face x=256) at the beam height
396
+ // (H/2): (20 + 256)/2 = 138. It is DERIVED from the
397
+ // geometry, so it moves whenever the detector does. The
398
+ // default centre is the CANVAS centre x=300, which sits
399
+ // PAST the detector: orbiting about it swings the
400
+ // experiment around a point outside itself and pushes
401
+ // the near objects into the lens. The experiment orbits
402
+ // about the experiment.
403
+ // azimuth -60 → sin az = -0.866 ⇒ camera x = 138 - 0.82·d ≈ -257 at
404
+ // d = 480: BEHIND the emitter, looking down +x through the
405
+ // slit wall (x = 96..160) toward the detector (x = 236).
406
+ // cos az = 0.5 swings it 30° off that axis in z — the
407
+ // "slightly rotated" part: dead-on, the slit wall would
408
+ // eclipse the detector completely; at 30° the detector's
409
+ // own hit map shows PAST the barrier edge, partially, which
410
+ // is exactly the invitation to orbit.
411
+ // elevation -18 → sin el < 0 ⇒ a smaller world y = ABOVE the beam plane
412
+ // (y-down): a gentle look down onto it, so the slit plate
413
+ // and the tunnels read as solids rather than edge-on lines.
414
+ // distance 480 → the full 256-px working length and the 400-px canvas
415
+ // height inside the fixed 50° vertical FOV, with the
416
+ // quanta streaming through the slits still legible. It
417
+ // frames the working length, so it is re-derived with the
418
+ // geometry — and by MEASUREMENT (project the scene's
419
+ // bounding box through the camera basis and read the frame
420
+ // fill: 480 gives max|ndc| 0.851), never by scaling the
421
+ // span, which over-corrects because the working length runs
422
+ // mostly ALONG the view direction and foreshortens.
423
+ // roll +90 → the SLIT-SEPARATION AXIS onto the screen HORIZONTAL.
424
+ // Derived from the scene like the rest, and forced by a
425
+ // constraint 2D imposes on 3D: 2D has only x and y, so with
426
+ // the beam on x the ONLY transverse axis available is y —
427
+ // the slits MUST split along y and the fringes MUST spread
428
+ // along y. 3D reuses that one setup (2D≡3D is this rung's
429
+ // headline), so it inherits a floor-to-ceiling wall with the
430
+ // detector eclipsed behind it. No orbit parameter can fix
431
+ // that: `elevation` moves the eye along ±y, which is the
432
+ // slit-separation axis itself, so "getting above it" slides
433
+ // the eye ALONG the pattern instead of over the wall. ROLL is
434
+ // the missing degree of freedom — it turns the VIEWER about
435
+ // the beam axis, leaving the world untouched, and lands world
436
+ // y (fringe spread) on the screen horizontal and world z (the
437
+ // corridor extrusion) on the vertical: a WIDE, low plate with
438
+ // the two slots SIDE BY SIDE and the fringes reading as bands
439
+ // left-to-right — the textbook DSE picture. The plate's
440
+ // on-screen bounding box goes 182×648 (tall) → 648×182 (wide);
441
+ // the framing is otherwise IDENTICAL, since a roll moves
442
+ // neither the eye nor the radius.
443
+ // THE SIGN is the visually approved one. Measured through
444
+ // `_orbitCameraBasis` at this exact pose (detector plate
445
+ // corners, 600×600): at roll +90 the low-y corner projects to
446
+ // screen-x 20.9 px and the high-y corner to 633.0 px (+y →
447
+ // RIGHT); at roll −90 they land at 579.1 px and −33.0 px
448
+ // (+y → LEFT). Both are equally "wide".
449
+ // preset: null — deliberately NOT a named preset. `setPreset` is a FULL
450
+ // framing reset: it forces `target: null` (and now `roll: 0`),
451
+ // which is exactly what makes the preset buttons the reliable
452
+ // way back — and exactly why they cannot express this pose,
453
+ // whose whole point is the scene-derived `target` above.
454
+ // NOT a compensator: the former azimuth:38 papered over a renderer defect
455
+ // (a ferried canvas drawn with a y-up UV assumption in the y-down world,
456
+ // on a single-sided plane), fixed at the root — orientation is derived in
457
+ // world space and both broad faces carry the map, so every orbit is now
458
+ // equally correct. This pose is a CHOICE about what to show first.
459
+ renderState: {
460
+ renderer: 'three',
461
+ camera: {
462
+ orbit: { azimuth: -60, elevation: -18, distance: 546, roll: 90,
463
+ target: { x: 177.5, y: 200, z: 0 } },
464
+ preset: null
465
+ }
466
+ }
467
+ },
468
+ { id: '2d', label: '2D' }
469
+ ]
470
+ },
471
+ {
472
+ id: 'dse-photoelectric',
473
+ title: 'Photoelectric',
474
+ group: 'DSE',
475
+ prose: 'The photoelectric effect as absorb/re-emit: an incoming light stream charges an ' +
476
+ 'ABSORBING wall — it fills to capacity (watch the stored bar plateau) and radiates ' +
477
+ 'lower-dimension quanta out its back, mass exactly conserved. A live chart shows the ' +
478
+ 'wall\'s charge ledger (stored / absorbed / radiated); a second Detector Screen behind ' +
479
+ 'the wall counts the radiated photoelectrons as they emerge past it. Incoming light ' +
480
+ 'charges the wall, radiated quanta appear beyond it and get counted — the current ' +
481
+ 'absorb/radiate drip (the emergent-probability threshold comes later).',
482
+ source: 'scenes/dse-photoelectric.js'
483
+ },
484
+ // ── The canonical-leg pair (CEO 2026-08-03). The competence baseline: no
485
+ // barrier, no slits, the plate ON the leg the ring formula was written for,
486
+ // and the graph a DSE researcher actually looks at first. 2D only — one
487
+ // declared world each, no mode strip (a physics variant is a recipe
488
+ // declaration, never a mode).
489
+ {
490
+ id: 'dse-one-emitter',
491
+ title: 'One Emitter at the Canonical Leg',
492
+ group: 'DSE',
493
+ prose: 'ONE point source, nothing between it and the plate, and the plate placed at the ' +
494
+ 'axial leg D = λ_eff/4 — the leg the published ring formula r_m = (λ/4)·√((2m+1)²+1) ' +
495
+ 'was written for, where the "+1" under the root IS the (λ/4)² axial term. At that leg ' +
496
+ 'the pattern crosses the wall at TWO points, one above and one below the source, each ' +
497
+ 'a quarter-wavelength out: the count-by-y chart standing behind the wall should show a ' +
498
+ 'band PAIR at y = 120 and y = 280 on the 600×400 stage, with a sharp minimum just ' +
499
+ 'inside each and a flat plateau between them. The plate COUNTS WITHOUT ABSORBING ' +
500
+ '(preset transparent-probe), so the reading is the density at the plate plane rather ' +
501
+ 'than a first-arrival caustic, and the quanta fly on to a far-edge sink. λ = 320 px is ' +
502
+ 'chosen for resolution: the dip-to-peak structure is 0.025λ wide, so a shorter ' +
503
+ 'wavelength would smear it into a hump on this stage. The light is yellow because its ' +
504
+ 'fullness is 0.3125 — derived, never forced.',
505
+ source: 'scenes/dse-one-emitter.js',
506
+ renderState: { render: [{ key: 'colorScheme', value: 'roygbiv' }] }
507
+ },
508
+ {
509
+ id: 'dse-two-emitters',
510
+ title: 'Two Emitters at the Canonical Leg',
511
+ group: 'DSE',
512
+ prose: 'The SAME apparatus with a SECOND point source, separated from the first by exactly ' +
513
+ 'd = λ_eff/2 — the strongest-coupling case, since two sources reinforce when their ' +
514
+ 'separation is a multiple of λ/2 and the coupling falls as (λ/d)². The plate stays at ' +
515
+ 'the same leg D = λ_eff/4 from the emitter plane, with the same chart registered to it. ' +
516
+ 'Because each source lays its own band a quarter-wavelength above and below itself, and ' +
517
+ 'the two sources sit a half-wavelength apart, source 1\'s LOWER band and source 2\'s ' +
518
+ 'UPPER band land on the SAME line — the midline y = 200, equidistant from both ' +
519
+ 'emitters. So the chart should show a CENTRAL peak where the one-emitter rung has none, ' +
520
+ 'flanked by the two outer bands at y = 40 and y = 360. That contrast — no centre with ' +
521
+ 'one source, a doubly-reinforced centre with two — is the whole claim, and it is one ' +
522
+ 'glance at two graphs.',
523
+ source: 'scenes/dse-two-emitters.js',
524
+ renderState: { render: [{ key: 'colorScheme', value: 'roygbiv' }] }
525
+ },
526
+ {
527
+ id: 'dse-two-slits',
528
+ title: 'Two Slits at the Canonical Leg',
529
+ group: 'DSE',
530
+ prose: 'The SAME apparatus as "Two Emitters at the Canonical Leg", but the two point ' +
531
+ 'sources are SLIT EXITS in an opaque barrier rather than two free emitters. ONE ' +
532
+ 'emitter sits behind the barrier and lights BOTH slits; each slit is a directed ' +
533
+ 'Tunnel channel whose exit gate is COLLAPSED and PHASE-RESET, so the mouth stands ' +
534
+ 'in for a phase-locked point source at exactly the slit\'s own y — the same ' +
535
+ '"configured emitter" composition dse-tunnel-rebuild uses. The slit centres sit at ' +
536
+ 'the identical y = 120 and y = 280 the two-emitter rung used, separated by the same ' +
537
+ 'd = λ_eff/2, and the plate sits at the identical leg D = λ_eff/4 — now measured from ' +
538
+ 'the slit exit plane, this apparatus\'s new source plane. If the slit is a faithful ' +
539
+ 'point source, the two rungs must draw the SAME picture: a central peak at y = 200 ' +
540
+ '(source 1\'s lower band and source 2\'s upper band coincide there) flanked by outer ' +
541
+ 'bands at y = 40 and y = 360.',
542
+ source: 'scenes/dse-two-slits.js',
543
+ renderState: { render: [{ key: 'colorScheme', value: 'roygbiv' }] }
544
+ },
545
+ {
546
+ id: 'dse-one-slit',
547
+ title: 'One Slit at the Canonical Leg',
548
+ group: 'DSE',
549
+ prose: 'The companion arm to "Two Slits at the Canonical Leg": the IDENTICAL apparatus ' +
550
+ '(same emitter, same barrier, same slit-1 aperture at y=120, same leg D, same ' +
551
+ 'plate, same chart) with the SECOND slit walled off — one aperture instead of two. ' +
552
+ 'A slit exit is a phase-locked point source at its own y, so with only slit 1 open ' +
553
+ 'the surviving population is governed by ONE source at y=120: its own ring ladder ' +
554
+ 'predicts bands at y=40, y=200, and y=360 (the same three y positions the two-slit ' +
555
+ 'rung shows), not a single broad structureless lobe centred on the open slit. This ' +
556
+ 'is the actual double-slit claim — presence and position of structure from purely ' +
557
+ 'stochastic, non-interacting particles — read as one glance between the two charts.',
558
+ source: 'scenes/dse-one-slit.js',
559
+ renderState: { render: [{ key: 'colorScheme', value: 'roygbiv' }] }
560
+ },
561
+
562
+ // ── Impossible things — the front-page scenes that were experiment pages (each a sketch
563
+ // file composing the SAME objects, params and seed as the page it replaced).
564
+ {
565
+ id: 'blackbody',
566
+ title: 'Blackbody Radiation',
567
+ group: 'Impossible things',
568
+ prose: 'A cavity of absorbing walls held at a target number of quanta by its own thermostat. ' +
569
+ 'Leaves collide and merge into parent frames; a frame\'s fullness is its frequency, so ' +
570
+ 'what leaks out of the slit is a spectrum of frame sizes. The Planck-like curve emerges ' +
571
+ 'from collisions — no energy quanta assumed.',
572
+ source: 'scenes/blackbody.js'
573
+ },
574
+ {
575
+ id: 'circuit-sat',
576
+ title: 'Circuit SAT: Half-Adder',
577
+ group: 'Impossible things',
578
+ prose: 'Two laser inputs drive rail traces into an XOR gate (SUM) and an AND gate (CARRY). A ' +
579
+ 'gate\'s valve opens only when its two input ammeters read its logic, so the circuit ' +
580
+ 'settles onto the half-adder\'s answer, read as the current on each grounded readout. ' +
581
+ 'No search algorithm runs.',
582
+ source: 'scenes/circuit-sat.js'
583
+ },
584
+ {
585
+ id: 'big-bang',
586
+ title: 'Big Bang',
587
+ group: 'Impossible things',
588
+ prose: 'One radial burst of leaves with no initial momentum, in a world that generates parent ' +
589
+ 'frames top-down. The uniform field clumps into filaments and voids with no gravity law ' +
590
+ 'and no cosmological constant.',
591
+ source: 'scenes/big-bang.js'
592
+ },
593
+ {
594
+ id: 'atomic-model',
595
+ title: 'Atomic Model',
596
+ group: 'Impossible things',
597
+ prose: 'Forty-eight quarks in a proton frame, the proton and one electron in a stationary atom ' +
598
+ 'frame. Each parent\'s compress pulls its children one pixel toward its centre each tick, ' +
599
+ 'so the structure binds and holds with no force law and no quantum numbers.',
600
+ source: 'scenes/atomic-model.js'
601
+ },
602
+ {
603
+ id: 'collision-matrix-3d',
604
+ title: '3D Collisions Lab',
605
+ group: 'Impossible things',
606
+ prose: 'Seven head-on lanes of equal-mass voxel pairs at seven depths, from feather-light to ' +
607
+ 'rigid. Density sets how hard the bodies are; contact is identical on every axis. The ' +
608
+ 'same engine that makes light makes mechanics.',
609
+ source: 'scenes/collision-matrix-3d.js',
610
+ renderState: { renderer: 'three' }
611
+ },
612
+ {
613
+ id: 'smoking-gun',
614
+ title: 'Smoking Gun',
615
+ group: 'Smoking Gun',
616
+ prose: 'A gun fires and the smoke emerges. Your challenge: make the bullet self-propelled.',
617
+ source: 'scenes/smoking-gun.js',
618
+ // A side profile centred on the muzzle (az 180: barrel +X reads screen-right), and the
619
+ // archived Full look halved, rays from the fundamental layer only.
620
+ renderState: {
621
+ renderer: 'three',
622
+ camera: { orbit: { azimuth: 180, elevation: 8, distance: 480, target: { x: 300, y: 300, z: 0 } },
623
+ preset: null },
624
+ render: renderRowsFromBag({ glowIntensity: 0.6, godrays: 0.5, godrayDensity: 0.8,
625
+ godraySource: 'leaf', heat: 0.2, smoke: 0 })
626
+ }
627
+ }
628
+ ];
629
+
630
+ // ── The mode contract (S3/W2, CEO-ratified 2026-07-26) ───────────────────────
631
+
632
+ /**
633
+ * walkthroughModes(rung) — the rung's declared mode list, ALWAYS an array.
634
+ * A rung without `modes` is single-mode: `[]` (the caller renders no UI and
635
+ * loads the base renderState). This is the ONE reader of the raw field.
636
+ * @param {object} rung
637
+ * @returns {Array<{id:string,label:string,renderState?:object}>}
638
+ */
639
+ export function walkthroughModes(rung) {
640
+ return (rung && Array.isArray(rung.modes)) ? rung.modes : [];
641
+ }
642
+
643
+ /**
644
+ * walkthroughGroup(rung) — the rung's LAB GROUP label, always a string.
645
+ *
646
+ * The ONE reader of the raw `group` field AND the ONE owner of the default label
647
+ * a rung that declares none falls into. Every surface that renders the rungs in
648
+ * groups (the STUDIO dropdown, the sdk showcase gallery) ferries THIS — spelling
649
+ * `rung.group || 'Light Lab'` at a call site is the schema-mirror anti-pattern
650
+ * (rename the default in one place and the other surface silently splits the
651
+ * group in two).
652
+ *
653
+ * @param {object} rung — a WALKTHROUGH entry.
654
+ * @returns {string}
655
+ */
656
+ export function walkthroughGroup(rung) {
657
+ return (rung && typeof rung.group === 'string' && rung.group.length > 0)
658
+ ? rung.group : 'Light Lab';
659
+ }
660
+
661
+ /**
662
+ * defaultWalkthroughModeId(rung) — the rung's DEFAULT mode id (the first declared
663
+ * entry), or null for a single-mode rung.
664
+ * @param {object} rung
665
+ * @returns {string|null}
666
+ */
667
+ export function defaultWalkthroughModeId(rung) {
668
+ var modes = walkthroughModes(rung);
669
+ return modes.length > 0 ? modes[0].id : null;
670
+ }
671
+
672
+ /**
673
+ * effectiveRenderState(rung, modeId) — THE ONE derivation point for "what
674
+ * renderState does this rung load in this mode".
675
+ *
676
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
677
+ * Merges the selected mode entry's `renderState` DELTA over the rung's base
678
+ * `renderState` and returns the NORMALIZED bag (`normalizeSavedRenderState`
679
+ * shape) the walkthrough load path already ferries. Merge rules:
680
+ * - scalar/object keys (renderer, seed, camera, backdropVisible, …):
681
+ * the mode entry WINS when it declares the key;
682
+ * - `render` (the ordered setRenderOption rows): base rows THEN mode rows —
683
+ * a concat, so a mode row applied later naturally overrides a base row with
684
+ * the same key (the same last-write-wins the replay already has).
685
+ *
686
+ * `modeId` may be null/undefined → the rung's DEFAULT mode (first entry), or the
687
+ * bare base bag for a single-mode rung.
688
+ *
689
+ * FAIL-LOUD: an undeclared `modeId` THROWS, naming the rung and its declared
690
+ * list. There is deliberately NO silent default-to-first for an unknown id — a
691
+ * stale mode id must surface, not quietly load the wrong scene.
692
+ *
693
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
694
+ * A renderState carries ZERO physics: it selects the renderer, the seed and the camera.
695
+ * The SCENE's world (and so its dimensionality) is its own co-init call, not here.
696
+ *
697
+ * @param {object} rung — a WALKTHROUGH entry.
698
+ * @param {string|null} [modeId] — a declared mode id, or null for the default.
699
+ * @returns {object} the normalized renderState bag.
700
+ */
701
+ export function effectiveRenderState(rung, modeId) {
702
+ var modes = walkthroughModes(rung);
703
+ var entry = null;
704
+ if (modeId === undefined || modeId === null) {
705
+ entry = modes.length > 0 ? modes[0] : null;
706
+ } else {
707
+ for (var i = 0; i < modes.length; i++) {
708
+ if (modes[i].id === modeId) { entry = modes[i]; break; }
709
+ }
710
+ if (!entry) {
711
+ throw new Error('[walkthrough] rung "' + (rung && rung.id) + '" has no mode "' +
712
+ modeId + '" — declared modes: [' +
713
+ modes.map(function (m) { return m.id; }).join(', ') +
714
+ ']. (FAIL-LOUD.)');
715
+ }
716
+ }
717
+ var base = (rung && rung.renderState) || {};
718
+ var delta = (entry && entry.renderState) || {};
719
+ var merged = Object.assign({}, base, delta);
720
+ // `render` is an ORDERED apply-list, not a scalar: concat rather than replace.
721
+ var baseRender = Array.isArray(base.render) ? base.render : [];
722
+ var deltaRender = Array.isArray(delta.render) ? delta.render : [];
723
+ if (baseRender.length || deltaRender.length) {
724
+ merged.render = baseRender.concat(deltaRender);
725
+ }
726
+ return normalizeSavedRenderState(merged);
727
+ }
728
+
729
+ // ── First contact (CEO 2026-07-26) ───────────────────────────────────────────
730
+
731
+ /**
732
+ * FIRST_CONTACT_WALKTHROUGH_ID — the rung STUDIO opens on with no deep link.
733
+ *
734
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
735
+ * The ONE authority for "what does a newcomer see when they open the IDE". It is
736
+ * a `walkthrough:<id>` selection value, so `AuthorView` loads it through the SAME
737
+ * dropdown path a user click takes — no second boot branch, no template id to keep
738
+ * in sync. Validated at module load (below): a typo fails loud immediately rather
739
+ * than silently landing on a blank canvas.
740
+ *
741
+ * ── Layer 2: what it shows ───────────────────────────────────────────────────
742
+ * The double-slit rebuilt on the Tunnel object, in its DEFAULT (3D) mode: the
743
+ * camera stands behind the emitter looking obliquely at the slit wall, and the
744
+ * interference pattern builds on the detector plate while you watch. This is
745
+ * PRODUCT INTENT — the first thing FRQTL shows is its signature experiment, framed
746
+ * so the pattern is already forming and the camera is obviously yours to move.
747
+ *
748
+ * @type {string}
749
+ */
750
+ export var FIRST_CONTACT_WALKTHROUGH_ID = 'dse-tunnel-rebuild';
751
+
752
+ /**
753
+ * walkthroughById(id) — id → rung, or null. The ONE resolver every consumer of a
754
+ * declared rung id (first contact, a deep link) ferries instead of
755
+ * re-writing `WALKTHROUGH.find(...)` at each call site.
756
+ * @param {string} id
757
+ * @returns {object|null}
758
+ */
759
+ export function walkthroughById(id) {
760
+ for (var i = 0; i < WALKTHROUGH.length; i++) {
761
+ if (WALKTHROUGH[i].id === id) { return WALKTHROUGH[i]; }
762
+ }
763
+ return null;
764
+ }
765
+
766
+ // FAIL-LOUD at module load for EVERY declared-id constant: a retirement that
767
+ // orphans one of these must break here, loudly, at import time — never silently
768
+ // downstream where a dead id turns into a blank stage or a self-embed.
769
+ [['FIRST_CONTACT_WALKTHROUGH_ID', FIRST_CONTACT_WALKTHROUGH_ID]
770
+ ].forEach(function (pair) {
771
+ if (!walkthroughById(pair[1])) {
772
+ throw new Error('[walkthrough] ' + pair[0] + ' "' + pair[1] +
773
+ '" is not a declared rung — declared ids: [' +
774
+ WALKTHROUGH.map(function (r) { return r.id; }).join(', ') + ']. (FAIL-LOUD.)');
775
+ }
776
+ });
777
+
778
+ // ── is3D normalization (E-W0a, CEO 2026-07-23; mode-aware S3/W2) ─────────────
779
+ // STUDIO must read ONE `is3D` field for a selection regardless of whether it came
780
+ // from the gallery registry (experiments.js `is3D`) or a walkthrough rung. A rung's
781
+ // 3D-ness is ALREADY declared by its (possibly mode-merged) `renderState.renderer` —
782
+ // three IS the 3D renderer, the ONE display fact the load path threads — so `is3D` here is
783
+ // DERIVED through `effectiveRenderState` for the rung's DEFAULT mode, never a
784
+ // hand-typed second source that could drift (schema-mirror kill). Stamped once at
785
+ // module load so every consumer sees the uniform field.
786
+ WALKTHROUGH.forEach(function (rung) {
787
+ rung.is3D = effectiveRenderState(rung, null).renderer === 'three';
788
+ });
789
+
790
+ /**
791
+ * walkthroughIs3D(id, modeId) — the ONE mount-time 3D predicate for a walkthrough
792
+ * rung, the peer of experiments.js `experimentIs3D`. Derived through
793
+ * `effectiveRenderState` (the single derivation point) for the given mode, or the
794
+ * rung's default mode when `modeId` is omitted. Returns false for an unknown id;
795
+ * THROWS (via effectiveRenderState) for a known id with an undeclared mode.
796
+ * @param {string} id — a rung id (the `walkthrough:<id>` selection value).
797
+ * @param {string|null} [modeId] — a declared mode id, or null for the default.
798
+ * @returns {boolean}
799
+ */
800
+ export function walkthroughIs3D(id, modeId) {
801
+ for (var i = 0; i < WALKTHROUGH.length; i++) {
802
+ if (WALKTHROUGH[i].id === id) {
803
+ return effectiveRenderState(WALKTHROUGH[i], modeId).renderer === 'three';
804
+ }
805
+ }
806
+ return false;
807
+ }