@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,110 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * copy-gate.js — the ONE owner of what shipped copy may look like: the placeholder rules and the
5
+ * segment shape. Every table of viewer-facing copy validates through it at load:
6
+ * lib/shim/registry/exhibits.js (the showcase roster, SHOWCASE_INTRO, SHOWCASE_THROUGH_LINE,
7
+ * SHOWCASE_INVITE) and lib/features/ide/presets.js (PRESET_EXPLAINERS).
8
+ *
9
+ * assertCopyText(text, where) → text (non-empty, no placeholder)
10
+ * assertCopyParagraphs(paragraphs, where, { table }) → paragraphs
11
+ *
12
+ * A paragraph is a non-empty segment array; a segment is a string or `{ text, href: URL|null,
13
+ * em?: true }` — a null href is a link not yet known, a string href is https and no stand-in. With
14
+ * `{ table: true }` a paragraph may also be a TABLE paragraph (`{ table: … }`): its structure is the
15
+ * run-once card's input contract (lib/components/run-once-card assertSpec), so this gate does NOT
16
+ * re-enumerate its keys — it walks every string leaf for placeholder text. With `{ table: false }`
17
+ * a table paragraph is refused outright.
18
+ *
19
+ * What a caption SAYS is the CEO's pass, never a gate here: this module checks shape only.
20
+ *
21
+ * @module copy-gate
22
+ */
23
+
24
+ /** Text that marks unfinished copy; shipped copy never contains it. */
25
+ export const PLACEHOLDER_TEXT = /\b(TODO|TBD|FIXME|XXX|placeholder|lorem)\b/i;
26
+
27
+ /** An href that stands in for a link not yet known (declare such a link `href: null` instead). */
28
+ export const PLACEHOLDER_HREF = /^(#|$)|example\.(com|org|net)|localhost|127\.0\.0\.1/i;
29
+
30
+ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
31
+
32
+ /**
33
+ * Non-empty text with no placeholder in it.
34
+ * @param {*} text
35
+ * @param {string} where names the copy in the error (e.g. '[exhibits] r8-sun.why')
36
+ * @returns {string} text
37
+ * @throws {Error}
38
+ */
39
+ export function assertCopyText(text, where) {
40
+ if (typeof text !== 'string' || text.trim() === '') {
41
+ throw new Error(where + ' must be non-empty text (got ' + JSON.stringify(text) + ').');
42
+ }
43
+ if (PLACEHOLDER_TEXT.test(text)) {
44
+ throw new Error(where + ': placeholder text in shipped copy: ' + JSON.stringify(text));
45
+ }
46
+ return text;
47
+ }
48
+
49
+ function assertSegment(seg, where) {
50
+ if (typeof seg === 'string') { assertCopyText(seg, where); return; }
51
+ if (!seg || typeof seg !== 'object' || typeof seg.text !== 'string') {
52
+ throw new Error(where + ': a segment is neither a string nor { text, href } (got ' + JSON.stringify(seg) + ').');
53
+ }
54
+ assertCopyText(seg.text, where);
55
+ if (!has(seg, 'href') || (seg.href !== null && typeof seg.href !== 'string')) {
56
+ throw new Error(where + ': link segment ' + JSON.stringify(seg.text) +
57
+ ' must declare href as a URL or null (not-yet-known link), got ' + JSON.stringify(seg.href) + '.');
58
+ }
59
+ if (has(seg, 'em') && seg.em !== true) {
60
+ throw new Error(where + ': segment ' + JSON.stringify(seg.text) +
61
+ ' declares em as ' + JSON.stringify(seg.em) + ' — em is true (emphasis) or absent.');
62
+ }
63
+ if (typeof seg.href === 'string' && (PLACEHOLDER_HREF.test(seg.href) || !/^https:\/\//.test(seg.href))) {
64
+ throw new Error(where + ': placeholder or non-https href ' + JSON.stringify(seg.href) +
65
+ ' — a link not yet known is declared null, never a stand-in URL.');
66
+ }
67
+ }
68
+
69
+ function walkLeaves(v, where) {
70
+ if (typeof v === 'string') {
71
+ if (PLACEHOLDER_TEXT.test(v)) { throw new Error(where + ': placeholder text in shipped copy: ' + JSON.stringify(v)); }
72
+ return;
73
+ }
74
+ if (v && typeof v === 'object') { Object.keys(v).forEach((k) => walkLeaves(v[k], where)); }
75
+ }
76
+
77
+ /**
78
+ * A non-empty list of paragraphs, every segment valid; table paragraphs only where allowed.
79
+ * @param {Array} paragraphs
80
+ * @param {string} where
81
+ * @param {{table: boolean}} opts whether a table paragraph is allowed here (required — no default)
82
+ * @returns {Array} paragraphs
83
+ * @throws {Error}
84
+ */
85
+ export function assertCopyParagraphs(paragraphs, where, opts) {
86
+ if (!opts || typeof opts.table !== 'boolean') {
87
+ throw new TypeError('copy-gate: assertCopyParagraphs(' + where + ') needs { table: true|false }.');
88
+ }
89
+ if (!Array.isArray(paragraphs) || paragraphs.length === 0) {
90
+ throw new Error(where + ' must be a non-empty array of paragraphs (got ' + JSON.stringify(paragraphs) + ').');
91
+ }
92
+ paragraphs.forEach((para, i) => {
93
+ const at = where + ' paragraph ' + i;
94
+ if (Array.isArray(para)) {
95
+ if (para.length === 0) { throw new Error(at + ' is an empty segment array.'); }
96
+ para.forEach((seg) => assertSegment(seg, at));
97
+ return;
98
+ }
99
+ if (para && typeof para === 'object' && has(para, 'table')) {
100
+ if (!opts.table) {
101
+ throw new Error(at + ' is a table paragraph — a table is allowed only in the run-once intro (SHOWCASE_INTRO); ' +
102
+ 'this copy is plain paragraphs.');
103
+ }
104
+ walkLeaves(para.table, at);
105
+ return;
106
+ }
107
+ throw new Error(at + ' is neither a segment array nor a table paragraph (got ' + JSON.stringify(para) + ').');
108
+ });
109
+ return paragraphs;
110
+ }
@@ -0,0 +1,455 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * exhibits.js — the SHOWCASE ROSTER ("Impossible things, running"), the run-once card of every
5
+ * roster scene the GYM plays, and the showcase copy around them (the intro card, the through-line
6
+ * and the one invitation). It is the ONE owner of every word of that copy.
7
+ *
8
+ * EXHIBITS is the ONE source for the front-page tiles, their GYM deep links and the scene card
9
+ * (CEO 2026-10-03): nine rows in front-page order, each
10
+ * `{ kind, id, mode, title, tile, what, why, how, controls, still }`.
11
+ * - `kind` is 'rung' (a walk-through rung, lib/shim/registry/walkthrough.js) or 'experiment'
12
+ * (a canonical experiment page, lib/shim/registry/experiments.js); `id` resolves there.
13
+ * - `mode` is a rung's declared mode (null for a single-mode rung and for an experiment).
14
+ * - `title` is the tile's and the card's title; `tile` is the published one-sentence tile line.
15
+ * - `what` (What you are seeing), `why` (Why it is impossible the usual way) and `how` (How FRQTL
16
+ * gets it) are the card's copy, CARD_PARTS, every one REQUIRED on every row.
17
+ * - `controls` (optional) is how to drive a scene's declared controls and what to read; only an
18
+ * interactive scene carries one.
19
+ * - `still` (optional) is the tile still: `{ tick, alt, pose? }` — the tick the tile-stills
20
+ * instrument (apps/ide/test/tile-stills-capture.spec.js) drives the scene to before capturing,
21
+ * the still's alt text (REQUIRED with a still; its one home), and on a 3D scene whose default
22
+ * camera hides its structure, the camera pose (a setOrbit patch).
23
+ *
24
+ * exhibitFor({ kind, id, modeId? }) → the roster row, or null (declared: the scene has no card)
25
+ * sceneKey({ kind, id, modeId? }) → the scene's run-once seen-key (the one mode normalizer)
26
+ * showcaseCard(row) → { kind, id, modeId, seenKey, title, paragraphs, is3D }
27
+ *
28
+ * The card is the row's title, `what`, `why`, `how`, `controls` (when present), JOYSTICK_EXPLAINER
29
+ * on a 3D scene (the pads themselves are CameraControls'), and SHOWCASE_INVITE last, by reference.
30
+ * It never renders registry prose. The bag's { seenKey, title, paragraphs } is what
31
+ * lib/components/run-once-card renders; SHOWCASE_INTRO is such a bag too (its own seen-key).
32
+ *
33
+ * Every word here is validated at load through lib/shim/registry/copy-gate.js (placeholder rules and
34
+ * segment shape). A table paragraph is allowed only in SHOWCASE_INTRO. The invitation and the
35
+ * intro's paragraphs appear ONCE: a row that pastes either is refused, as is an intro that pastes
36
+ * the through-line.
37
+ *
38
+ * Consumers: the GYM (lib/features/ide/CompareView.jsx — the intro card, the scene card and the scene
39
+ * select's leading rows); the roster tests (lib/shim/registry/test/exhibits.test.mjs,
40
+ * lib/frqtl/scenes/test/scene-files.test.mjs) and the GYM specs under apps/ide/test that walk it;
41
+ * the front-page generator and gates, which read this module as SERVED by a built bundle
42
+ * (shim/registry/exhibits.js).
43
+ *
44
+ * WHAT IS NOT HERE: the IDE's first-contact default — a walkthrough rung declared once at
45
+ * lib/shim/registry/walkthrough.js `FIRST_CONTACT_WALKTHROUGH_ID`.
46
+ *
47
+ * @module exhibits
48
+ */
49
+
50
+ import { EXPERIMENTS, experimentById, experimentIs3D } from './experiments.js';
51
+ import {
52
+ WALKTHROUGH, walkthroughById, walkthroughModes, defaultWalkthroughModeId, walkthroughIs3D, effectiveRenderState
53
+ } from './walkthrough.js';
54
+ import { assertCopyText, assertCopyParagraphs } from './copy-gate.js';
55
+
56
+ /** The scene kinds a roster row (and a player deep link) can name. */
57
+ export const EXHIBIT_KINDS = Object.freeze(['rung', 'experiment']);
58
+
59
+ /** The copy parts a run-once CARD shows, in order; every roster row carries all of them. */
60
+ export const CARD_PARTS = Object.freeze(['what', 'why', 'how']);
61
+
62
+ const segText = (para) => para.map((s) => (typeof s === 'string' ? s : s.text)).join('');
63
+
64
+ /**
65
+ * A plain showcase paragraph: one segment array of STRINGS (no table, no link), through the copy gate.
66
+ * @returns the paragraph
67
+ */
68
+ export function assertPlainParagraph(para, where) {
69
+ assertCopyParagraphs([para], where, { table: false });
70
+ if (!para.every((s) => typeof s === 'string')) {
71
+ throw new Error(where + ' is plain text: strings only (the page has no link renderer).');
72
+ }
73
+ return para;
74
+ }
75
+
76
+ /** The one invitation, shown as the LAST paragraph of every scene card (by reference, never pasted). */
77
+ export const SHOWCASE_INVITE = Object.freeze([
78
+ "Every scene here is the same few objects at whatever scale you declare: open it, read its code, change anything, and decide for yourself.",
79
+ ]);
80
+ assertPlainParagraph(SHOWCASE_INVITE, '[exhibits] SHOWCASE_INVITE');
81
+
82
+ /** The through-line under the front page's grid: one paragraph of plain text. */
83
+ export const SHOWCASE_THROUGH_LINE = Object.freeze([
84
+ "FRQTL is fractal, as the name sounds. Everything in it is built from three primitives, ticks, pixels and occupancy, and none of them carries a unit. Its layers nest by powers of two, each twice the size of the one below. So the engine does not know or care what scale a scene is: the sun on this page is the same 3D sphere emitter, made of the same primitives, whether you read it as a star, a candle flame or an atom. Every scene is independent cellular automata following local rules, with no equations, and every scene is the same few objects, composed at whatever scale you declare.",
85
+ ]);
86
+ assertPlainParagraph(SHOWCASE_THROUGH_LINE, '[exhibits] SHOWCASE_THROUGH_LINE');
87
+
88
+ /** The caption of the intro's estimate table. */
89
+ const SHOWCASE_ESTIMATE_CAPTION = "A back-of-the-envelope estimate.";
90
+
91
+ /**
92
+ * assertShowcaseIntro — the intro card bag: seen-key, title and paragraphs through the copy gate
93
+ * (a table paragraph allowed; its structure is the run-once card's contract), and never a paste of
94
+ * the through-line. Exported so a test can show each refusal on a planted copy.
95
+ * @returns the intro
96
+ */
97
+ export function assertShowcaseIntro(intro) {
98
+ if (!intro || typeof intro.seenKey !== 'string' || intro.seenKey === '') {
99
+ throw new Error('[exhibits] SHOWCASE_INTRO needs a non-empty seenKey.');
100
+ }
101
+ assertCopyText(intro.title, '[exhibits] SHOWCASE_INTRO.title');
102
+ assertCopyParagraphs(intro.paragraphs, '[exhibits] SHOWCASE_INTRO', { table: true });
103
+ const through = segText(SHOWCASE_THROUGH_LINE);
104
+ intro.paragraphs.forEach((para, i) => {
105
+ if (Array.isArray(para) && segText(para).indexOf(through) !== -1) {
106
+ throw new Error('[exhibits] SHOWCASE_INTRO paragraph ' + i + ' pastes SHOWCASE_THROUGH_LINE — the through-line has one place.');
107
+ }
108
+ });
109
+ return intro;
110
+ }
111
+
112
+ /** The public EGPT repository (DeSciX/EGPT, main) the intro's closing paragraph cites, one document per link. */
113
+ const EGPT_EVIDENCE_BLOB = 'https://github.com/DeSciX/EGPT/blob/main/';
114
+
115
+ /** A link segment to one tracked document of the public EGPT repository. */
116
+ const egptDoc = (text, path) => Object.freeze({ text, href: EGPT_EVIDENCE_BLOB + path });
117
+
118
+ /** The run-once intro card, shown once before the first roster scene card a viewer meets. */
119
+ export const SHOWCASE_INTRO = Object.freeze({
120
+ seenKey: 'showcase-intro',
121
+ title: "Why these are considered \"impossible\"",
122
+ paragraphs: Object.freeze([
123
+ Object.freeze([
124
+ "The industry-standard projection is that simulating just 100 electrons requires a classical computer with 2^100 bits, more atoms than there are in the universe (",
125
+ Object.freeze({ text: "Microsoft's quantum computing overview", href: "https://learn.microsoft.com/en-us/azure/quantum/overview-understanding-quantum-computing" }),
126
+ "). Google's framing is that 100 photons (or qubits) would require 725 trillion times the age of the universe to calculate using the world's most powerful supercomputers, like Frontier.",
127
+ ]),
128
+ Object.freeze(["You're about to see the FRQTL hyper-computer accurately simulate many thousands on your own mobile phone or laptop."]),
129
+ Object.freeze(["FRQTL is a patent-pending P=NP hyper-computer based on a radically different architecture than current computer chips and hypothetical qubit-based quantum computers."]),
130
+ Object.freeze(["FRQTL doesn't cheat, and it isn't like any \"traditional\" approach such as ray tracing, path tracing and photon mapping, which cannot produce interference or wave diffusion at any cost. They treat light as straight lines. Real-time engines fake shadows, reflections and indirect light as separate effects, with fixed bounce limits."]),
131
+ Object.freeze(["Matching what you will see FRQTL do here means solving Maxwell's equations directly. The standard method is FDTD: a 3D grid of about 20 cells per wavelength, updated every time step. At real optical wavelengths that cost is out of reach."]),
132
+ Object.freeze(["Assumptions: visible light at 500 nm; 20 cells per wavelength; about 24 bytes and 30 operations per cell per step; a time step at the standard stability limit for a 3D grid, so light advances at most 1/√3 of a cell per step and crossing the scene takes about 1.7 steps for every cell across it; a Frontier-class supercomputer (about 10^18 operations per second, about 10 PB of memory). \"One crossing\" means light travelling once across the scene."]),
133
+ Object.freeze({ table: Object.freeze({
134
+ caption: SHOWCASE_ESTIMATE_CAPTION,
135
+ head: Object.freeze(["Scene","Grid cells","Memory needed","Compute for one crossing","Time on Frontier"]),
136
+ rows: Object.freeze([
137
+ Object.freeze(["10 cm tabletop","6 × 10^19","about 1.5 zettabytes, about 150,000× Frontier's memory","about 10^28 operations","about 400 years"]),
138
+ Object.freeze(["5 m room","8 × 10^24","about 2 × 10^26 bytes, more than all the storage on Earth","about 10^35 operations","about 3 billion years"]),
139
+ ]),
140
+ }) }),
141
+ Object.freeze(["Running indefinitely multiplies that by every further crossing. At physical wavelengths, \"impossible for supercomputers\" is literally true."]),
142
+ Object.freeze(["The table is about light waves. The blackbody, the logic circuit, the Big Bang and the atom are impossible the usual way for a different reason: each needs a theory, equations and a program of its own, at a scale of its own. The glowing box needs energy quanta assumed, the circuit needs a search through its inputs, the cosmos needs gravity computed over 13.8 billion years, and the atom needs quantised energy levels."]),
143
+ Object.freeze([
144
+ "The EGPT proofs are machine-checked in Lean. Rota's Entropy Theorem (RET), the Lean theorem ",
145
+ egptDoc("rota_all_entropy_scaled_shannon", "08_LEAN/EGPT/InformationTheory/EntropyNumber/RotaEntropy.lean"),
146
+ ", is why P=NP is quantum computing: by Rota's proof, all the distributions of physics are scaled Shannon entropy (for the Bose-Einstein, Fermi-Dirac and Maxwell-Boltzmann distributions, the Lean theorems ",
147
+ egptDoc("entropy_BE_eq_C_shannon, entropy_FD_eq_C_shannon and entropy_MB_eq_C_shannon", "08_LEAN/EGPT/InformationTheory/Physics/PhysicsDist.lean"),
148
+ "), and all entropy, including von Neumann entropy, is scaled Shannon entropy. RET provides the framework for all three P=NP proofs, the Lean theorems ",
149
+ egptDoc("P_eq_NP", "08_LEAN/EGPT/InformationTheory/Complexity/SetRFL.lean"),
150
+ ", ",
151
+ egptDoc("P_eq_NP_info", "08_LEAN/EGPT/InformationTheory/Complexity/PPNP.lean"),
152
+ " and ",
153
+ egptDoc("P_eq_NP_info_standard", "08_LEAN/EGPT/InformationTheory/Complexity/StandardComplexity.lean"),
154
+ ". Together, RET and the distribution theorems are a formalisation of a Theory of Everything, as discussed under Question 1 of ",
155
+ egptDoc("the Claude Singularity session", "12_SINGULARITY/Claude/Cowork_Session_10.01.2026.md"),
156
+ ". The proof of P=NP was first outlined in the paper P Probably Equals NP (January 2025).",
157
+ ]),
158
+ ]),
159
+ });
160
+ assertShowcaseIntro(SHOWCASE_INTRO);
161
+
162
+ // The authored roster, front-page order. `title` and `tile` are the published front-page tile copy
163
+ // (egpt-frqtl site/index.html #proof at 5d2f019: each tile's <b> and its sentence), verbatim;
164
+ // `what`, `why`, `how`, `controls` and each `still.alt` are the CEO-passed captions.
165
+ const ROSTER = [
166
+ {
167
+ kind: 'rung', id: "dse-tunnel-rebuild", mode: "3d",
168
+ still: { tick: 7200, alt: "Three red blocks with blue slit walls between them inside a dark glass box; a cloud of yellow dots hangs above the slits, and below them a flat heat-map plate of blue squares glows yellow-green along a central band." },
169
+ title: "Double slit",
170
+ tile: "Two slits, one particle at a time. Feynman's “only mystery,” with no amplitudes to add.",
171
+ what: "Particles go through two slits one at a time, and bright and dark bands build up on the detector plate below them, live in your browser tab: the pattern Feynman called quantum mechanics' \"only mystery\". EGPT conceptually holds computer science and physics are linked by P=NP and the principle of least action - that is, everything in reality is efficiently computable. The Lean RET, the derivation of EntropyReal from an IIDParticleSource, and the P=NP capstones are modern formalizations of proofs and ideas advanced by Gian-Carlo Rota circa the late 1970s. The EGPT Lean proofs transparently show the most rigorously advanced proofs ever offered for bridging not only quantum and classical mechanics but mathematics and all the other sciences. Watch it run, read its code, and judge for yourself whether this is true P=NP quantum computing.",
172
+ why: "Feynman called it \"impossible, absolutely impossible, to explain in any classical way\". A ray tracer cannot draw it at all, and solving the light waves directly for a 10 cm tabletop would need about 150,000 times the memory of the Frontier supercomputer and about 400 years of its time.",
173
+ how: "A sphere emitter, walls, two tunnels for the slits and a detector plate. Each particle is an independent cellular automaton following local rules: no wave equation, no amplitudes to add.",
174
+ },
175
+ {
176
+ kind: 'rung', id: "r8-sun",
177
+ still: { tick: 300, alt: "A white-hot sun with a golden halo and fine rays fanning out in every direction across a dim, grainy space." },
178
+ title: "A sun",
179
+ tile: "Put light in the scene and let light do what light does. No Standard Model; god-rays from particle density.",
180
+ what: "Put light in the scene and let light do what light does: a blazing sun, its glow and god rays spreading in every direction through the space around it.",
181
+ why: "Games paint glow and god rays onto the finished picture as a separate screen effect, aimed from where the sun is on the screen rather than from any light in the scene.",
182
+ how: "One sphere emitter, shining in every direction. The god rays are rendered from the density of the particles themselves. No Standard Model, no equations, and the engine does not know whether you are looking at a star, a candle flame or an atom.",
183
+ },
184
+ {
185
+ kind: 'rung', id: "cl1d-mirror-route", mode: "3d",
186
+ still: { tick: 1200, alt: "Ten brightly coloured mirrors in a ring, seen from above; a dense band of gold, orange and green dots circles a dark, empty centre inside it, and faint gold and green rays fan out from that light between the mirrors into the dark space around." },
187
+ title: "Infinite mirror",
188
+ tile: "Ten mirrors in a ring. Light walks the loop. No Fresnel equations.",
189
+ what: "Ten mirrors in a ring. Light goes round and round the loop, reflected and diffused from mirror to mirror, each mirror colouring the light it sends on.",
190
+ why: "Conventional renderers cut every light path off after a set number of bounces and treat shadows, reflections and indirect light as separate effects. Solving the light directly in a 5 m room would take Frontier about 3 billion years.",
191
+ how: "One emitter and ten walls set as mirrors. Light leaves in every direction; shadowing, reflection and obstruction are all live, with no limit on the objects in its path, and the diffusion is real. No Fresnel equations.",
192
+ },
193
+ {
194
+ kind: 'rung', id: "r5-fountain-pool",
195
+ still: { tick: 300, alt: "A dark teal surface scattered with white glints like light on water, with a faint ring around a dark dot at its centre." },
196
+ title: "A pool",
197
+ tile: "One source. Rings. Interference as the geometry of random walks, not a wave.",
198
+ what: "One source, and rings of ripples spread out across the water, glinting like light on a pool.",
199
+ why: "A ray is a straight line with no wave in it, so ray tracing cannot make interference rings at any cost, and solving the waves directly runs into the costs in the intro.",
200
+ how: "One sphere emitter - classical mechanics that looks like water, from the same light object, the sphere emitter that made the sun in the previous scene. Every particle zig-zags as it travels, so its path is itself a wave; the rings are the geometry of those wavy random walks crossing. The water is the same light field shown through a different renderer. No equations.",
201
+ },
202
+ {
203
+ kind: 'rung', id: "blackbody",
204
+ still: { tick: 600, alt: "A grey box packed with glowing red blobs and tiny yellow dots, with dots and a few red blobs streaming out of an opening on its right side." },
205
+ title: "Blackbody",
206
+ tile: "Particles in a box. The Planck curve appears; no quanta assumed.",
207
+ what: "A hot box of jostling particles glows red, and light spills out through a slit in its side. The Planck curve appears; no energy quanta are assumed.",
208
+ why: "Classical physics, the Rayleigh–Jeans law, predicts infinite energy at high frequencies, the \"ultraviolet catastrophe\". There is no settled traditional way to compute the plasma physics behind Planck's 1900 law from first principles.",
209
+ how: "A box, a thermostat that keeps it hot, and an absorbing wall that catches whatever escapes. Colliding particles merge into larger frames, and how full a frame is sets its colour. Independent cellular automata, no equations.",
210
+ },
211
+ {
212
+ kind: 'rung', id: "circuit-sat",
213
+ still: { tick: 300, alt: "A circuit of blue bars on black; two lit input lines on the left feed two gates, the top output line carries a long stream of orange beads to the right, and the bottom output holds only a short, stalled cluster." },
214
+ title: "Circuit SAT",
215
+ tile: "A half-adder settles on its own answer. No search algorithm runs.",
216
+ what: "A half-adder settles on its own answer, you are watching electricity simulated in a circuit component live in your browser tab. Quanta (electrons or photons if you like) flow down the wires into two gates; with input A on, the SUM line fills with flowing quanta and nothing flows along CARRY. No search algorithm runs.",
217
+ why: "Circuit satisfiability, the canonical P vs NP problem of computer science, is also a physical question about real circuits. Every modern circuit is built out of half-adders - put some constant number C of half-adders together and if simulating one is polynomial then C × polynomial is still polynomial. No polynomial-time algorithm is supposed to exist, so the usual way to find an input that satisfies a circuit is to search for one. The EGPT proofs, machine-checked in Lean, show that P=NP.",
218
+ how: "Two logic gates, the wires between them and an emitter on each input - the half-adder electrical circuit simulation settles in your browser in polynomial time, effectively at once. A gate opens only when its inputs carry the pattern it stands for, so the circuit settles into its answer. Independent cellular automata, no equations. EGPT conceptually holds computer science and physics are linked by P=NP and the principle of least action - that is, everything in reality is efficiently computable. The Lean RET, the derivation of EntropyReal from an IIDParticleSource, and the P=NP capstones are modern formalizations of proofs and ideas advanced by Gian-Carlo Rota circa the late 1970s. The EGPT Lean proofs transparently show the most rigorously advanced proofs ever offered for bridging not only quantum and classical mechanics but mathematics and all the other sciences. Watch the answer emerge with no search, read the code, and judge for yourself whether this is true P=NP quantum computing.",
219
+ controls: "It starts with input A on. Click the input A and input B toggles (bottom left) to switch each input on or off. Read the two outputs on the right: the top one is SUM (A xor B), the bottom one is CARRY (A and B). An output with quanta flowing along it reads 1; an empty one reads 0.",
220
+ },
221
+ {
222
+ kind: 'rung', id: "big-bang",
223
+ still: { tick: 200, alt: "A ragged red cloud of overlapping glowing discs laced with fine gold specks, clumped and broken at its edges, on black." },
224
+ title: "Big Bang",
225
+ tile: "A uniform field. Filaments and voids form. No gravity law, no constant.",
226
+ what: "One burst fills the space, and structure emerges on its own. This is a fractal at its heart - it could be quantum emergence or large cosmic structures ... one set of rules from which attractive forces are a matter of your chosen unit of scale. This simulation is not meant as an assertion of what the correct scales are; it is an exploration of how emergence and scale are related.",
227
+ why: "Large-scale cosmic structure requires gravitational attraction tuned over 13.8 billion years, and a simulation has to compute that gravity across the whole span.",
228
+ how: "One point source, and a world that builds larger frames from the top down. No gravity law, no cosmological constant, no equations.",
229
+ },
230
+ {
231
+ kind: 'rung', id: "atomic-model",
232
+ still: { tick: 600, alt: "A large dark-red disc with a brighter red core; at the core's left edge a small violet ball sits in a tight cluster of pale beads, and one lone pale bead lies further out at lower right." },
233
+ title: "Atomic model",
234
+ tile: "Particles bind and stay bound. Stable orbits without quantum numbers.",
235
+ what: "Quarks pack into a proton and stay bound, and an electron keeps its place in the atom around it. Stable orbits, without quantum numbers. Like the Big Bang, this simulation is not meant as an assertion of what the correct scales are; it is an exploration of how emergence and scale are related - the engine lets you set stable containment, or experiment with scale so that stability is emergent.",
236
+ why: "Classical physics says an orbiting electron radiates its energy away and spirals into the nucleus in about 16 trillionths of a second. Textbook physics avoids that only by assuming quantised energy levels.",
237
+ how: "Frames nested three layers deep: quarks in a proton, the proton and an electron in an atom. Each parent draws its children one step toward its centre every tick, and binding emerges from that. No force equation.",
238
+ },
239
+ {
240
+ kind: 'rung', id: "collision-matrix-3d",
241
+ still: { tick: 200, alt: "Pairs of shiny balls, red, orange, yellow, green and violet, meeting in mid-air against black.", pose: { azimuth: 45, elevation: -30 } },
242
+ title: "3D Collisions Lab",
243
+ tile: "Balls in a box. Contact, momentum, settling. The same engine that makes light makes mechanics; no second physics.",
244
+ what: "Balls in a box, in seven lanes coloured red to violet by density. Each pair flies together and collides: the hard ones bounce cleanly, the soft ones press into each other before parting. Contact, momentum, settling. This is a \"Collisions Lab\" because it is up to you to find the capacity-and-fill relationships that produce the desired elastic vs inelastic collisions at your desired scale.",
245
+ why: "In a game engine, collisions come from a separate physics engine, such as NVIDIA PhysX, solving rigid-body equations beside the renderer that draws the light. Two systems, two sets of rules.",
246
+ how: "The same engine that makes the light in the other scenes makes these collisions: pairs of the same frames at seven densities. No second physics, no equations.",
247
+ },
248
+ ];
249
+
250
+ const ROW_KEYS = ['kind', 'id', 'mode', 'title', 'tile', 'what', 'why', 'how', 'controls', 'still'];
251
+
252
+ /**
253
+ * A tile still: absent, or `{ tick: <positive integer>, alt: <text>, pose?: <orbit patch> }` (THROWS
254
+ * otherwise). `alt` is the still's alt text, required whenever a still is declared. `pose` is a
255
+ * non-empty object of finite numbers handed VERBATIM to the scene's camera handle
256
+ * (`runner.cameraControl.setOrbit`), whose provider owns the orbit keys; the instrument asserts the
257
+ * camera's read-back agrees with every key it names.
258
+ */
259
+ function stillOf(v, where) {
260
+ if (v === undefined || v === null) { return null; }
261
+ const keys = (v && typeof v === 'object') ? Object.keys(v) : [];
262
+ const shape = '{ tick: <positive integer>, alt: <text>, pose?: <non-empty setOrbit patch> } or absent';
263
+ const unknown = keys.filter((k) => k !== 'tick' && k !== 'alt' && k !== 'pose');
264
+ if (unknown.length || !Number.isInteger(v.tick) || v.tick < 1) {
265
+ throw new Error('[exhibits] ' + where + ' must be ' + shape + ' (got ' + JSON.stringify(v) + ').');
266
+ }
267
+ const alt = assertCopyText(v.alt, '[exhibits] ' + where + '.alt');
268
+ if (v.pose === undefined) { return Object.freeze({ tick: v.tick, alt }); }
269
+ const pose = v.pose;
270
+ if (!pose || typeof pose !== 'object' || Array.isArray(pose) || Object.keys(pose).length === 0) {
271
+ throw new Error('[exhibits] ' + where + '.pose must be a non-empty setOrbit patch (got ' + JSON.stringify(pose) + ').');
272
+ }
273
+ Object.keys(pose).forEach((k) => {
274
+ if (typeof pose[k] !== 'number' || !Number.isFinite(pose[k])) {
275
+ throw new Error('[exhibits] ' + where + '.pose.' + k + ' must be a finite number (got ' + String(pose[k]) + ').');
276
+ }
277
+ });
278
+ return Object.freeze({ tick: v.tick, alt, pose: Object.freeze(Object.assign({}, pose)) });
279
+ }
280
+
281
+ function rungOrThrow(id) {
282
+ const rung = walkthroughById(id);
283
+ if (!rung) {
284
+ throw new Error('[exhibits] unknown walk-through rung "' + id + '" — declared rungs: [' +
285
+ WALKTHROUGH.map((r) => r.id).join(', ') + '].');
286
+ }
287
+ return rung;
288
+ }
289
+
290
+ function experimentOrThrow(id) {
291
+ const entry = experimentById(id);
292
+ if (!entry) {
293
+ throw new Error('[exhibits] unknown experiment "' + id + '" — declared experiments: [' +
294
+ EXPERIMENTS.map((e) => e.id).join(', ') + ']. Add it to lib/shim/registry/experiments.js first.');
295
+ }
296
+ return entry;
297
+ }
298
+
299
+ /** A rung's effective mode: the declared one (validated), or null for a single-mode rung. */
300
+ function rungMode(rung, modeId) {
301
+ const eff = (modeId !== undefined && modeId !== null) ? modeId : defaultWalkthroughModeId(rung);
302
+ effectiveRenderState(rung, eff); // FAIL-LOUD on an undeclared mode (its contract)
303
+ return walkthroughModes(rung).length > 1 ? eff : null;
304
+ }
305
+
306
+ /** Copy text through the copy gate: `required` refuses an absent value; an optional one is null when absent. */
307
+ function text(v, where, required) {
308
+ if ((v === undefined || v === null) && !required) { return null; }
309
+ return assertCopyText(v, '[exhibits] ' + where);
310
+ }
311
+
312
+ /** Refuse a row part that pastes the invitation or a whole intro paragraph (each has one place). */
313
+ function assertNotPasted(part, where) {
314
+ if (part.indexOf(segText(SHOWCASE_INVITE)) !== -1) {
315
+ throw new Error('[exhibits] ' + where + ' pastes SHOWCASE_INVITE — the card appends it by reference.');
316
+ }
317
+ SHOWCASE_INTRO.paragraphs.forEach((para, i) => {
318
+ if (Array.isArray(para) && part.indexOf(segText(para)) !== -1) {
319
+ throw new Error('[exhibits] ' + where + ' pastes SHOWCASE_INTRO paragraph ' + i + ' — the intro\'s text has one place.');
320
+ }
321
+ });
322
+ }
323
+
324
+ /**
325
+ * assertRoster — the load-time validator: every row's kind and id resolve in their registry, a
326
+ * rung's mode is declared, an experiment carries none, no scene repeats, no unknown field, every
327
+ * row has a title, a tile line and every CARD_PARTS part, `controls` is non-empty when present, all
328
+ * copy passes the copy gate, and no part pastes the invitation or an intro paragraph. Exported so a
329
+ * test can show each refusal on a planted roster. @returns the normalized, frozen rows (with `order`).
330
+ */
331
+ export function assertRoster(rows) {
332
+ const seen = new Set();
333
+ return Object.freeze(rows.map((row, order) => {
334
+ Object.keys(row).forEach((k) => {
335
+ if (ROW_KEYS.indexOf(k) === -1) {
336
+ throw new Error('[exhibits] row ' + order + ' has unknown field "' + k + '" — fields: ' + ROW_KEYS.join(', ') + '.');
337
+ }
338
+ });
339
+ if (EXHIBIT_KINDS.indexOf(row.kind) === -1) {
340
+ throw new Error('[exhibits] row ' + order + ' kind ' + JSON.stringify(row.kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
341
+ }
342
+ let mode = null;
343
+ if (row.kind === 'rung') {
344
+ mode = rungMode(rungOrThrow(row.id), row.mode);
345
+ } else {
346
+ experimentOrThrow(row.id);
347
+ if (row.mode !== undefined) {
348
+ throw new Error('[exhibits] experiment row "' + row.id + '" names a mode — only a rung has modes.');
349
+ }
350
+ }
351
+ if (row.title === undefined || row.title === null) {
352
+ throw new Error('[exhibits] row "' + row.id + '" has no title — every card is titled.');
353
+ }
354
+ const title = text(row.title, row.id + '.title', true);
355
+ const key = row.kind + ':' + row.id + ':' + mode;
356
+ if (seen.has(key)) { throw new Error('[exhibits] row ' + key + ' appears twice.'); }
357
+ seen.add(key);
358
+ const out = { kind: row.kind, id: row.id, mode, order, title };
359
+ CARD_PARTS.forEach((p) => { out[p] = text(row[p], row.id + '.' + p, true); });
360
+ out.controls = text(row.controls, row.id + '.controls', false);
361
+ out.tile = text(row.tile, row.id + '.tile', true);
362
+ CARD_PARTS.concat(['controls']).forEach((p) => { if (out[p] !== null) { assertNotPasted(out[p], row.id + '.' + p); } });
363
+ out.still = stillOf(row.still, row.id + '.still');
364
+ return Object.freeze(out);
365
+ }));
366
+ }
367
+
368
+ /** The showcase roster, validated and normalized at load. */
369
+ export const EXHIBITS = assertRoster(ROSTER);
370
+
371
+ /** The roster rows of one kind, in roster order (throws on an unknown kind). */
372
+ export function exhibitsOfKind(kind) {
373
+ if (EXHIBIT_KINDS.indexOf(kind) === -1) {
374
+ throw new Error('[exhibits] exhibitsOfKind: unknown kind ' + JSON.stringify(kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
375
+ }
376
+ return EXHIBITS.filter((e) => e.kind === kind);
377
+ }
378
+
379
+ /**
380
+ * The roster row for a scene, or null when the scene is not on the roster. A null is a DECLARED
381
+ * outcome the caller records (the player plays such a scene with no card: 'not-offered-no-roster-row').
382
+ * The scene itself must exist: an unknown kind, id or mode throws.
383
+ * @param {{kind: 'rung'|'experiment', id: string, modeId?: string|null}} scene
384
+ */
385
+ export function exhibitFor(scene) {
386
+ const kind = scene && scene.kind;
387
+ if (EXHIBIT_KINDS.indexOf(kind) === -1) {
388
+ throw new Error('[exhibits] unknown scene kind ' + JSON.stringify(kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
389
+ }
390
+ let mode = null;
391
+ if (kind === 'rung') { mode = rungMode(rungOrThrow(scene.id), scene.modeId); } else { experimentOrThrow(scene.id); }
392
+ for (let i = 0; i < EXHIBITS.length; i++) {
393
+ const e = EXHIBITS[i];
394
+ if (e.kind === kind && e.id === scene.id && e.mode === mode) { return e; }
395
+ }
396
+ return null;
397
+ }
398
+
399
+ /**
400
+ * How the MOVE/LOOK pads work, shown on every 3D roster scene's card. The claims are
401
+ * CameraControls.js's contract and are exercised in apps/ide/test/gym-scene-player.spec.js
402
+ * (MOVE pad and W/A/S/D translate, LOOK pad and the arrow keys rotate; mouse and touch both drive
403
+ * the pads).
404
+ */
405
+ export const JOYSTICK_EXPLAINER = Object.freeze([
406
+ Object.freeze([
407
+ 'Move and Look: this scene is 3D. Drag the MOVE pad (bottom left) to walk the camera forward, back ' +
408
+ 'and sideways, and the LOOK pad (bottom right) to turn it. Both pads take a finger on a phone or ' +
409
+ 'the mouse on a desktop; on a keyboard, W/A/S/D move and the arrow keys look.',
410
+ ]),
411
+ ]);
412
+
413
+ /**
414
+ * sceneKey — the run-once seen-key of a scene: kind, id and, for a multi-mode rung, its EFFECTIVE
415
+ * mode (the one normalizer, rungMode); never a bare id, so it cannot collide with a tab key.
416
+ * Throws for an unknown kind, id or mode.
417
+ * @param {{kind: 'rung'|'experiment', id: string, modeId?: string|null}} scene
418
+ */
419
+ export function sceneKey(scene) {
420
+ const kind = scene && scene.kind;
421
+ if (EXHIBIT_KINDS.indexOf(kind) === -1) {
422
+ throw new Error('[exhibits] unknown scene kind ' + JSON.stringify(kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
423
+ }
424
+ let mode = null;
425
+ if (kind === 'rung') { mode = rungMode(rungOrThrow(scene.id), scene.modeId); } else { experimentOrThrow(scene.id); }
426
+ return 'scene-' + kind + '-' + scene.id + (mode ? '-' + mode : '');
427
+ }
428
+
429
+ /**
430
+ * The run-once card of a ROSTER row: title, then `what`, `why`, `how`, `controls` when present, the
431
+ * JOYSTICK_EXPLAINER on a 3D scene (3D read from walkthroughIs3D / experimentIs3D), and
432
+ * SHOWCASE_INVITE last, by reference. Composed from the row and the showcase constants only: no
433
+ * registry prose ever. The bag's { seenKey, title, paragraphs } is what lib/components/run-once-card
434
+ * renders.
435
+ * @param {object} row an element of EXHIBITS (exhibitFor's answer)
436
+ * @throws {Error} a row that is not on the roster.
437
+ */
438
+ export function showcaseCard(row) {
439
+ if (EXHIBITS.indexOf(row) === -1) {
440
+ throw new Error('[exhibits] showcaseCard takes a roster row (an EXHIBITS element, e.g. exhibitFor(scene)); got ' +
441
+ JSON.stringify(row && { kind: row.kind, id: row.id }) + '.');
442
+ }
443
+ const is3D = row.kind === 'rung' ? walkthroughIs3D(row.id, row.mode) : experimentIs3D(row.id);
444
+ let paragraphs = CARD_PARTS.map((p) => Object.freeze([row[p]]));
445
+ if (row.controls !== null) { paragraphs = paragraphs.concat([Object.freeze([row.controls])]); }
446
+ if (is3D) { paragraphs = paragraphs.concat(JOYSTICK_EXPLAINER); }
447
+ paragraphs = paragraphs.concat([SHOWCASE_INVITE]);
448
+ return Object.freeze({
449
+ kind: row.kind, id: row.id, modeId: row.mode,
450
+ seenKey: sceneKey({ kind: row.kind, id: row.id, modeId: row.mode }),
451
+ title: row.title,
452
+ paragraphs: Object.freeze(paragraphs),
453
+ is3D,
454
+ });
455
+ }