@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,33 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/render/sun-look.js — the ONE owner of the SUN's look: its render-option bag.
5
+ *
6
+ * The values are the FINAL FX bag EXACTLY as the CEO played and captured it in his export session
7
+ * (new_sun.png is the primary oracle; sun.png stays the historical dense reference), verbatim from
8
+ * his exported renderState. They are in the write door's shape — a BAG, what
9
+ * `runner.setRenderOptions(bag)` takes — and every key is a capability the three renderer declares.
10
+ *
11
+ * SunLook.SUN_RENDER — the sun's whole look (rung r8-sun's render rows).
12
+ * SunLook.SUN_GODRAYS — the god-ray pair, PICKED from SUN_RENDER: what another scene lights up
13
+ * "at the sun's settings" with (the mirror's 3D mode, the 3D DSE's switch).
14
+ *
15
+ * `godraySource` is NOT here: what the rays are computed from is each borrower's declared choice.
16
+ * The camera is NOT here: a pose is a rung row's view, not a look.
17
+ *
18
+ * Readers: `lib/shim/registry/walkthrough.js` (a named import) and a scene sketch (`SunLook`, the
19
+ * bridge's publication of this module until the game layer imports it). Zero physics: render
20
+ * options are read only by the renderer.
21
+ */
22
+
23
+
24
+ var SUN_RENDER = Object.freeze({
25
+ shape: 'sphere', colorScheme: 'warmcool',
26
+ godrays: 1.08, godrayDensity: 1.085,
27
+ glowIntensity: 0.93, heat: 0.35, smoke: 0
28
+ });
29
+
30
+ // The pair other scenes light up "at the sun's settings" with — PICKED from SUN_RENDER, never re-typed.
31
+ var SUN_GODRAYS = Object.freeze({ godrays: SUN_RENDER.godrays, godrayDensity: SUN_RENDER.godrayDensity });
32
+
33
+ export { SUN_RENDER, SUN_GODRAYS };
@@ -0,0 +1,262 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/render-state/apply.js — the ONE owner of the renderState contract AND of the procedure
5
+ * that applies a renderState bag to a booted runner.
6
+ *
7
+ * A renderState bag (renderer/seed load-time selections + render rows + camera + backdrop
8
+ * visibility + page controls) is replayed by every host: the GYM rung frame, STUDIO, the bundle's
9
+ * `bootRecipe`, the hosted eyes. They all come through `RenderState.apply` — directly (the bundle,
10
+ * which is its own page) or through the blank page's door `FrqtlPage.applyRenderState`
11
+ * (lib/frqtl/experiments/empty/index.html). A host never restates the procedure.
12
+ *
13
+ * ── Layer 1: API contract (named ES-module exports) ─────────────────────────────────────────────
14
+ *
15
+ * apply(runner, bag, env) → { applied, retained, camera, preset, backdrop }
16
+ * bag — ANY renderState (raw or normalized); normalized here, so a registry bag and a saved
17
+ * project's bag take one path.
18
+ * env — { knownNames: string[] }: `RenderCapabilitiesCommon.knownRenderCapabilityNames(
19
+ * providers)` — the page's renderer registry is a page global, not the runner's.
20
+ * Everything is RESOLVED before anything is written: a row no renderer declares, a camera
21
+ * on a runner with no `cameraControl`, or a camera handle with no `presets` THROWS with the
22
+ * runner untouched.
23
+ * Rows → `partitionRenderRows` against `runner.renderCapabilities()`, then ONE
24
+ * `runner.setRenderOptions(applied)` batch.
25
+ * Camera → `pickCameraReplay`: a preset the booted camera offers (`cameraControl.presets`)
26
+ * → `setPreset`, camera:'preset'; a preset it does not offer is NOT applied,
27
+ * camera:'retained' (the CALLER decides whether that is acceptable); an orbit →
28
+ * `setOrbit`, camera:'orbit'; no camera → null. `preset` names the preset applied
29
+ * or retained (null for an orbit or no camera).
30
+ * Backdrop → `backdropOf(runner)`: a boolean `backdropVisible` →
31
+ * `runner.updateObjectProperty(lo, 'visible', v)`, backdrop:v. An ABSENT
32
+ * `backdropVisible` (null after normalizing) is not written — the scene's own
33
+ * `addBackdrop(…, {visible})` rules — and reports backdrop:null, as does no backdrop LO.
34
+ * `pageControls` are NOT applied here: they are STUDIO's host-specific override.
35
+ *
36
+ * backdropOf(runner) — THE answer to "which LO is the backdrop" for a host: the
37
+ * census LO marked `backdrop === true`, or null. STUDIO's
38
+ * save captures its visibility through it, so capture and
39
+ * replay read one fact.
40
+ * normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag (never throws).
41
+ * pickCameraReplay(saved) — pure decision: named preset over raw orbit.
42
+ * partitionRenderRows(rows, declared, known) — apply / retain / throw, before anything is written.
43
+ * renderRowsFromBag(bag) — a { key: value } render-option bag as ordered rows.
44
+ *
45
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────────────────────────
46
+ *
47
+ * Zero physics. Replaying a value has the IDENTICAL effect as a user re-dragging the same control;
48
+ * the runner's own doors (`setRenderOptions`, `cameraControl`, `updateObjectProperty`) do the work.
49
+ */
50
+
51
+
52
+ /** @returns {boolean} */
53
+ function _isPlainObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
54
+
55
+ /**
56
+ * @param {{camera?: {orbit?:object|null, preset?:string|null}|null}} saved — a normalized bag.
57
+ * @returns {{mode:'preset', name:string}|{mode:'orbit', orbit:object}|null}
58
+ * `null` — no camera was captured (nothing to replay).
59
+ */
60
+ function pickCameraReplay(saved) {
61
+ if (!saved || !saved.camera) { return null; }
62
+ var cam = saved.camera;
63
+ if (typeof cam.preset === 'string' && cam.preset.length > 0) {
64
+ return { mode: 'preset', name: cam.preset };
65
+ }
66
+ if (cam.orbit && typeof cam.orbit === 'object') {
67
+ return { mode: 'orbit', orbit: cam.orbit };
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Tolerant normalizer for a saved module's `renderState` field — a stored record may be
74
+ * `undefined` (a pre-S6 legacy module), `null`, or a shape from another `version` — NEVER throws;
75
+ * every missing/malformed field degrades to a NEUTRAL default (empty array / null), one `apply`
76
+ * writes nothing for.
77
+ *
78
+ * @param {*} raw
79
+ * @returns {{
80
+ * version: number,
81
+ * renderer: string|null,
82
+ * engine: string|null,
83
+ * seed: string|null,
84
+ * backdropVisible: boolean|null,
85
+ * camera: {orbit:object|null, preset:string|null}|null,
86
+ * render: {key:string, value:*}[],
87
+ * pageControls: {id:string, value:*, bind:{object:string,prop:string}|null}[]
88
+ * }}
89
+ */
90
+ function normalizeSavedRenderState(raw) {
91
+ var r = _isPlainObject(raw) ? raw : {};
92
+ var camera = null;
93
+ if (_isPlainObject(r.camera)) {
94
+ camera = {
95
+ orbit: _isPlainObject(r.camera.orbit) ? r.camera.orbit : null,
96
+ preset: (typeof r.camera.preset === 'string') ? r.camera.preset : null
97
+ };
98
+ }
99
+ return {
100
+ version: (typeof r.version === 'number') ? r.version : 1,
101
+ renderer: (typeof r.renderer === 'string' && r.renderer.length > 0) ? r.renderer : null,
102
+ engine: (typeof r.engine === 'string' && r.engine.length > 0) ? r.engine : null,
103
+ seed: (r.seed !== undefined && r.seed !== null && r.seed !== '') ? String(r.seed) : null,
104
+ backdropVisible: (typeof r.backdropVisible === 'boolean') ? r.backdropVisible : null,
105
+ camera: camera,
106
+ render: Array.isArray(r.render) ? r.render.filter(function (row) {
107
+ return _isPlainObject(row) && typeof row.key === 'string' && row.key.length > 0;
108
+ }) : [],
109
+ pageControls: Array.isArray(r.pageControls) ? r.pageControls.filter(function (row) {
110
+ return _isPlainObject(row) && typeof row.id === 'string' && row.id.length > 0;
111
+ }).map(function (row) {
112
+ return {
113
+ id: row.id,
114
+ value: row.value,
115
+ bind: (_isPlainObject(row.bind) && typeof row.bind.object === 'string' &&
116
+ typeof row.bind.prop === 'string') ? { object: row.bind.object, prop: row.bind.prop } : null
117
+ };
118
+ }) : []
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Resolve a bag's `render[]` rows against the renderer that will RECEIVE them, before anything is
124
+ * written. Each row falls in exactly one class:
125
+ * - declared by the target → `apply` (a `{key: value}` bag for ONE
126
+ * `runner.setRenderOptions` call; a later row wins)
127
+ * - declared by another renderer → `retained` (NOT applied; the host decides what to do)
128
+ * - declared by no renderer → THROWS (a typo, or a deleted option)
129
+ *
130
+ * @param {{key:string, value:*}[]} rows
131
+ * @param {string[]} declaredNames — the target's `renderCapabilities()` names
132
+ * @param {string[]} knownNames — `RenderCapabilitiesCommon.knownRenderCapabilityNames(registry)`
133
+ * @returns {{apply: Object<string, *>, retained: {key:string, value:*}[]}}
134
+ * @throws {Error} on a row whose key no registered renderer declares, or bad arguments.
135
+ */
136
+ function partitionRenderRows(rows, declaredNames, knownNames) {
137
+ if (!Array.isArray(rows) || !Array.isArray(declaredNames) || !Array.isArray(knownNames)) {
138
+ throw new Error('[render-state] partitionRenderRows(rows, declaredNames, knownNames) takes three ' +
139
+ 'arrays. (FAIL-LOUD.)');
140
+ }
141
+ var apply = {};
142
+ var retained = [];
143
+ for (var i = 0; i < rows.length; i++) {
144
+ var row = rows[i];
145
+ if (declaredNames.indexOf(row.key) !== -1) {
146
+ apply[row.key] = row.value;
147
+ } else if (knownNames.indexOf(row.key) !== -1) {
148
+ retained.push({ key: row.key, value: row.value });
149
+ } else {
150
+ throw new Error('[render-state] saved render option "' + row.key + '" is declared by no ' +
151
+ 'registered renderer (known: [' + knownNames.join(', ') + ']). (FAIL-LOUD: a typo or a ' +
152
+ 'deleted option would otherwise silently change the picture.)');
153
+ }
154
+ }
155
+ return { apply: apply, retained: retained };
156
+ }
157
+
158
+ /**
159
+ * renderRowsFromBag(bag) — a render-option BAG (`{ key: value }`, the `runner.setRenderOptions`
160
+ * shape) as the ordered `[{ key, value }]` render rows a renderState carries, in the bag's own key
161
+ * order. The ONE place a bag becomes rows, so a look owned as a bag (lib/shim/render/sun-look.js)
162
+ * reaches a rung's renderState without a hand-typed row list.
163
+ *
164
+ * @param {Object<string, *>} bag
165
+ * @returns {{key:string, value:*}[]}
166
+ * @throws {Error} on a non-object or an array.
167
+ */
168
+ function renderRowsFromBag(bag) {
169
+ if (!bag || typeof bag !== 'object' || Array.isArray(bag)) {
170
+ throw new Error('[render-state] renderRowsFromBag(bag) takes a plain { key: value } object; got ' +
171
+ (Array.isArray(bag) ? 'an array' : String(bag)) + '. (FAIL-LOUD.)');
172
+ }
173
+ return Object.keys(bag).map(function (k) { return { key: k, value: bag[k] }; });
174
+ }
175
+
176
+ /** The backdrop LargeObject in the runner's census (the one with `backdrop === true`), or null. */
177
+ function backdropOf(runner) {
178
+ var los = runner && runner.largeObjects;
179
+ if (!Array.isArray(los)) {
180
+ throw new Error('[render-state] RenderState.backdropOf: runner.largeObjects is not the world\'s census ' +
181
+ '(an array) — the backdrop is found there and nowhere else. (FAIL-LOUD.)');
182
+ }
183
+ for (var i = 0; i < los.length; i++) {
184
+ if (los[i] && los[i].backdrop === true) { return los[i]; }
185
+ }
186
+ return null;
187
+ }
188
+
189
+ /**
190
+ * Apply a renderState bag to a booted runner — see the Layer 1 contract above.
191
+ *
192
+ * @param {object} runner — a booted FrqtlApp (its provider attached, its scene built).
193
+ * @param {*} bag — a renderState bag, raw or normalized.
194
+ * @param {{knownNames:string[]}} env
195
+ * @returns {{applied:Object<string,*>, retained:{key:string,value:*}[],
196
+ * camera:'preset'|'orbit'|'retained'|null, preset:string|null, backdrop:boolean|null}}
197
+ * @throws {Error} on bad arguments, a row no renderer declares, a value its descriptor refuses, a
198
+ * camera on a runner with no camera control, or any write the runner refuses.
199
+ */
200
+ function apply(runner, bag, env) {
201
+ if (!runner || typeof runner.renderCapabilities !== 'function' || typeof runner.setRenderOptions !== 'function') {
202
+ throw new Error('[render-state] RenderState.apply(runner, bag, env): runner must be a booted FrqtlApp ' +
203
+ '(renderCapabilities + setRenderOptions). (FAIL-LOUD.)');
204
+ }
205
+ if (!env || !Array.isArray(env.knownNames)) {
206
+ throw new Error('[render-state] RenderState.apply(runner, bag, env): env.knownNames must be the page\'s ' +
207
+ 'RenderCapabilitiesCommon.knownRenderCapabilityNames(providers) array. (FAIL-LOUD.)');
208
+ }
209
+ var n = normalizeSavedRenderState(bag);
210
+
211
+ // ── RESOLVE everything before anything is written ──
212
+ // Rows — partitioned against the booted renderer.
213
+ var parts = partitionRenderRows(
214
+ n.render,
215
+ runner.renderCapabilities().map(function (c) { return c.name; }),
216
+ env.knownNames
217
+ );
218
+ // Camera — preset over orbit; a preset the booted camera does not offer is reported, not applied.
219
+ var cam = pickCameraReplay(n);
220
+ var ctl = null, camera = null, preset = null;
221
+ if (cam) {
222
+ ctl = runner.cameraControl;
223
+ if (!ctl) {
224
+ throw new Error('[render-state] the bag declares a camera (' + JSON.stringify(n.camera) + ') but the ' +
225
+ 'runner has no cameraControl — a declared view on a display with no pose control would look ' +
226
+ 'fine and be wrong. (FAIL-LOUD.)');
227
+ }
228
+ if (cam.mode === 'preset') {
229
+ if (!Array.isArray(ctl.presets)) {
230
+ throw new Error('[render-state] the booted camera publishes no `presets` array — every ' +
231
+ 'getCameraControl() handle declares the presets it offers. (FAIL-LOUD.)');
232
+ }
233
+ preset = cam.name;
234
+ camera = (ctl.presets.indexOf(cam.name) === -1) ? 'retained' : 'preset';
235
+ } else {
236
+ camera = 'orbit';
237
+ }
238
+ }
239
+ // Backdrop — the one derivation: the census LO marked `backdrop === true`.
240
+ var lo = backdropOf(runner);
241
+
242
+ // ── WRITE ──
243
+ runner.setRenderOptions(parts.apply); // ONE validated batch
244
+ if (camera === 'preset') { ctl.setPreset(cam.name); }
245
+ else if (camera === 'orbit') { ctl.setOrbit(cam.orbit); }
246
+ var backdrop = null;
247
+ if (lo && n.backdropVisible !== null) {
248
+ runner.updateObjectProperty(lo, 'visible', n.backdropVisible);
249
+ backdrop = n.backdropVisible;
250
+ }
251
+
252
+ return { applied: parts.apply, retained: parts.retained, camera: camera, preset: preset, backdrop: backdrop };
253
+ }
254
+
255
+ export {
256
+ apply,
257
+ backdropOf,
258
+ normalizeSavedRenderState,
259
+ pickCameraReplay,
260
+ partitionRenderRows,
261
+ renderRowsFromBag
262
+ };
@@ -0,0 +1,74 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/render-state/index.js — the ESM door to the renderState contract.
5
+ *
6
+ * The contract and the APPLY procedure have ONE home: `lib/shim/render-state/apply.js`, an ES module
7
+ * the page (through experiments/_shared/boot.js), the parent realm and the sdk bundle all import.
8
+ * This module re-exports its namespace and members by name, so importers (STUDIO's save-state, the
9
+ * bundle codec, the walkthrough registry, the sdk esm entry) read the same object — never a copy.
10
+ *
11
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
12
+ *
13
+ * RenderState — the owner object: RenderState.apply(runner, bag, env).
14
+ * normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag (never throws).
15
+ * pickCameraReplay(saved) — pure decision: named preset over raw orbit.
16
+ * partitionRenderRows(rows, declared, known)
17
+ * — resolve render rows against the TARGET renderer.
18
+ * renderRowsFromBag(bag) — a { key: value } bag as ordered [{ key, value }] rows.
19
+ * applySavedUrlOverrides(src, over) — pure URL-string mutation for the load-TIME
20
+ * renderer/seed query params (lives here: the page never
21
+ * builds its own iframe src).
22
+ *
23
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
24
+ *
25
+ * Zero physics — data shaping over a renderState bag.
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ // The one owner of the renderState contract and its apply procedure.
31
+ import * as RenderState from './apply.js';
32
+ import { normalizeSavedRenderState, pickCameraReplay, partitionRenderRows, renderRowsFromBag } from './apply.js';
33
+
34
+ // ── applySavedUrlOverrides ───────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Set (or leave untouched) the `renderer`/`seed` query params on an
38
+ * experiment iframe `src` — the load-TIME axis of a saved render-state bag.
39
+ *
40
+ * @param {string} src — an iframe `src` (with or without an existing `?query`).
41
+ * @param {{renderer?:string|null, seed?:number|string|null}} [overrides]
42
+ * @returns {string} `src` with the requested overrides applied. Identical to
43
+ * `src` when `overrides` is absent or every field is falsy/undefined.
44
+ */
45
+ function applySavedUrlOverrides(src, overrides) {
46
+ if (typeof src !== 'string' || !overrides) { return src; }
47
+ if (Object.prototype.hasOwnProperty.call(overrides, 'egpt3D')) {
48
+ throw new Error('[render-state] applySavedUrlOverrides: `egpt3D` is DELETED — 3D-ness is the scene\'s ' +
49
+ 'co-init call (exp.createScene), and the display is `renderer` (three IS the 3D display). (FAIL-LOUD.)');
50
+ }
51
+ var qIdx = src.indexOf('?');
52
+ var base = qIdx === -1 ? src : src.slice(0, qIdx);
53
+ var qs = qIdx === -1 ? '' : src.slice(qIdx + 1);
54
+ var params = new URLSearchParams(qs);
55
+
56
+ if (overrides.renderer) { params.set('renderer', overrides.renderer); }
57
+ if (overrides.seed !== undefined && overrides.seed !== null && overrides.seed !== '') {
58
+ params.set('seed', String(overrides.seed));
59
+ }
60
+
61
+ var qsOut = params.toString();
62
+ return qsOut ? (base + '?' + qsOut) : base;
63
+ }
64
+
65
+ // ── Export ────────────────────────────────────────────────────────────────────
66
+
67
+ export {
68
+ RenderState,
69
+ normalizeSavedRenderState,
70
+ pickCameraReplay,
71
+ applySavedUrlOverrides,
72
+ partitionRenderRows,
73
+ renderRowsFromBag
74
+ };
@@ -0,0 +1,207 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/rpc — the request/response call() primitive (WS gym-controls-as-sugar
5
+ * Bite 3 Stage 2, Layer-A follow-up fix-loop; architect gate 5 charter —
6
+ * "the reqId/pending/timeout request/response-call()-over-postMessage pattern
7
+ * is promoted to a SHIM PRIMITIVE the moment a second consumer needs it").
8
+ *
9
+ * Peer of `lib/shim/controls`, `lib/shim/frame-data`, `lib/shim/expr/ast-schema`,
10
+ * `lib/shim/page-params` — same layout convention: named ES-module exports, imported by
11
+ * page-context sugar (`lib/frqtl/parts/FrqtlControlsPanel.js`) and the IDE alike.
12
+ *
13
+ * ── API ──────────────────────────────────────────────────────────────────────
14
+ *
15
+ * makeInAppRpc(dispatch) → { call(fn,args)->Promise, dispose() }
16
+ * Same-realm (same window) calling — `dispatch` runs directly, wrapped in
17
+ * a Promise for a uniform caller shape (no postMessage round-trip; there
18
+ * is no cross-realm boundary to cross).
19
+ *
20
+ * makeChannelRpc(channel, opts) → { call(fn,args)->Promise, dispose() }
21
+ * Cross-realm calling over any `{post,subscribe}` channel (same- or
22
+ * cross-origin — the mechanism does not care which). Builds the
23
+ * reqId/pending/timeout request/response protocol; rejects on timeout
24
+ * (`opts.callTimeoutMs`, default 3000) if the other side never replies —
25
+ * the ONE MUST-HAVE this primitive adds over a bare channel: an
26
+ * unresponsive/not-yet-booted target must not hang a caller's Promise
27
+ * forever.
28
+ *
29
+ * installRpcResponder(channel, dispatch, opts) → unsubscribeFn
30
+ * The REALM/receiving side's counterpart to `makeChannelRpc`: listens on
31
+ * `channel` for calls tagged `opts.tag`, invokes `dispatch(fn,args)`
32
+ * (sync OR Promise-returning — both supported uniformly), replies with
33
+ * the result or error. `dispatch`'s return value must be plain,
34
+ * structured-clone-serializable data (this module does not itself assert
35
+ * that — see the design-note below).
36
+ *
37
+ * makeWindowPostMessageChannel(targetWindow, opts) → {post,subscribe}
38
+ * A ready-made `window.postMessage`/`'message'`-event channel targeting
39
+ * one specific window (an iframe's `contentWindow`, or `window.parent`)
40
+ * — the shape `makeChannelRpc`/`installRpcResponder` accept.
41
+ *
42
+ * `opts.tag` is REQUIRED on `makeChannelRpc`/`installRpcResponder` — a short
43
+ * string namespacing this RPC user's postMessage traffic so two independent
44
+ * RPC users sharing one `window.postMessage` bus (e.g. this primitive AND
45
+ * the notebook realm's own `__frqtlRealm`-tagged traffic) never collide.
46
+ *
47
+ * ── Why this module exists ──────────────────────────────────────────────────
48
+ *
49
+ * `frqtl-controls-panel.js` once built this request/response protocol INLINE; the
50
+ * protocol is a shim primitive so the panel and every later consumer share one
51
+ * reqId/pending/timeout implementation.
52
+ */
53
+
54
+
55
+
56
+ /**
57
+ * makeInAppRpc(dispatch) — same-realm (same window) RPC.
58
+ * @param {(fn: string, args: any[]) => *} dispatch — invoked directly; may
59
+ * return a value or a Promise.
60
+ * @returns {{call: (fn: string, args: any[]) => Promise<*>, dispose: () => void}}
61
+ * @throws {Error} FAIL-LOUD if `dispatch` is not a function.
62
+ */
63
+ function makeInAppRpc(dispatch) {
64
+ if (typeof dispatch !== 'function') {
65
+ throw new Error('[shim/rpc] makeInAppRpc requires a dispatch function. (FAIL-LOUD.)');
66
+ }
67
+ return {
68
+ call: function (fn, args) {
69
+ try { return Promise.resolve(dispatch(fn, args)); }
70
+ catch (e) { return Promise.reject(e); }
71
+ },
72
+ dispose: function () {}
73
+ };
74
+ }
75
+
76
+ /**
77
+ * makeChannelRpc(channel, opts) — cross-realm RPC over a `{post,subscribe}`
78
+ * channel (see `makeWindowPostMessageChannel` for a ready-made one).
79
+ * @param {{post: (msg:*) => void, subscribe: (handler:(msg:*)=>void) => (() => void)}} channel
80
+ * @param {object} opts
81
+ * @param {string} opts.tag — REQUIRED: a unique namespace for this RPC's wire traffic.
82
+ * @param {number} [opts.callTimeoutMs=3000] — per-call timeout.
83
+ * @returns {{call: (fn: string, args: any[]) => Promise<*>, dispose: () => void}}
84
+ * @throws {Error} FAIL-LOUD if `channel` lacks post/subscribe, or `opts.tag` is missing.
85
+ */
86
+ function makeChannelRpc(channel, opts) {
87
+ opts = opts || {};
88
+ if (!channel || typeof channel.post !== 'function' || typeof channel.subscribe !== 'function') {
89
+ throw new Error('[shim/rpc] makeChannelRpc requires a channel with post/subscribe. (FAIL-LOUD.)');
90
+ }
91
+ var tag = opts.tag;
92
+ if (!tag) {
93
+ throw new Error('[shim/rpc] makeChannelRpc requires opts.tag (a unique namespace so ' +
94
+ 'multiple RPC users sharing one postMessage bus never collide). (FAIL-LOUD.)');
95
+ }
96
+ var timeoutMs = (typeof opts.callTimeoutMs === 'number' && opts.callTimeoutMs > 0) ? opts.callTimeoutMs : 3000;
97
+ var pending = Object.create(null);
98
+ var reqCounter = 0;
99
+ var unsub = channel.subscribe(function (msg) {
100
+ if (!msg || msg.__rpcTag !== tag || msg.type !== 'rpc:result') { return; }
101
+ var p = pending[msg.reqId];
102
+ if (!p) { return; }
103
+ delete pending[msg.reqId];
104
+ if (msg.error) { p.reject(new Error('[shim/rpc] remote: ' + msg.error)); }
105
+ else { p.resolve(msg.value); }
106
+ });
107
+ return {
108
+ call: function (fn, args) {
109
+ return new Promise(function (resolve, reject) {
110
+ var reqId = ++reqCounter;
111
+ var timer = setTimeout(function () {
112
+ if (!pending[reqId]) { return; } // already settled by a real reply
113
+ delete pending[reqId];
114
+ reject(new Error('[shim/rpc] "' + fn + '" timed out after ' + timeoutMs +
115
+ 'ms (tag "' + tag + '"). No responder installed on the other side? ' +
116
+ '(installRpcResponder)'));
117
+ }, timeoutMs);
118
+ pending[reqId] = {
119
+ resolve: function (v) { clearTimeout(timer); resolve(v); },
120
+ reject: function (e) { clearTimeout(timer); reject(e); }
121
+ };
122
+ channel.post({ __rpcTag: tag, type: 'rpc:call', reqId: reqId, fn: fn, args: args });
123
+ });
124
+ },
125
+ dispose: function () { if (typeof unsub === 'function') { unsub(); } }
126
+ };
127
+ }
128
+
129
+ /**
130
+ * installRpcResponder(channel, dispatch, opts) — the REALM/receiving side.
131
+ * @param {{post: Function, subscribe: Function}} channel
132
+ * @param {(fn: string, args: any[]) => *} dispatch — may return a value or a Promise.
133
+ * @param {object} opts
134
+ * @param {string} opts.tag — MUST match the caller's `makeChannelRpc` tag.
135
+ * @returns {() => void} unsubscribe function.
136
+ * @throws {Error} FAIL-LOUD if `channel` lacks post/subscribe, or `opts.tag` is missing.
137
+ */
138
+ function installRpcResponder(channel, dispatch, opts) {
139
+ opts = opts || {};
140
+ if (!channel || typeof channel.post !== 'function' || typeof channel.subscribe !== 'function') {
141
+ throw new Error('[shim/rpc] installRpcResponder requires a channel with post/subscribe. (FAIL-LOUD.)');
142
+ }
143
+ if (typeof dispatch !== 'function') {
144
+ throw new Error('[shim/rpc] installRpcResponder requires a dispatch function. (FAIL-LOUD.)');
145
+ }
146
+ var tag = opts.tag;
147
+ if (!tag) {
148
+ throw new Error('[shim/rpc] installRpcResponder requires opts.tag. (FAIL-LOUD.)');
149
+ }
150
+ return channel.subscribe(function (msg) {
151
+ if (!msg || msg.__rpcTag !== tag || msg.type !== 'rpc:call') { return; }
152
+ var reqId = msg.reqId;
153
+ Promise.resolve().then(function () {
154
+ return dispatch(msg.fn, msg.args);
155
+ }).then(function (value) {
156
+ channel.post({ __rpcTag: tag, type: 'rpc:result', reqId: reqId, value: value });
157
+ }, function (e) {
158
+ channel.post({
159
+ __rpcTag: tag, type: 'rpc:result', reqId: reqId,
160
+ error: (e && e.message) ? e.message : String(e)
161
+ });
162
+ });
163
+ });
164
+ }
165
+
166
+ /**
167
+ * makeWindowPostMessageChannel(targetWindow, opts) — a ready-made
168
+ * `{post,subscribe}` channel over `window.postMessage`/`'message'` events,
169
+ * scoped to one specific `targetWindow` (an iframe's `contentWindow`, or
170
+ * `window.parent`) — messages from any OTHER source are ignored.
171
+ * @param {Window} targetWindow — the window to post to / accept messages from.
172
+ * @param {object} [opts]
173
+ * @param {Window} [opts.selfWindow] — defaults to the global `window`.
174
+ * @returns {{post: Function, subscribe: Function}}
175
+ * @throws {Error} FAIL-LOUD if `targetWindow` or the self window is missing.
176
+ */
177
+ function makeWindowPostMessageChannel(targetWindow, opts) {
178
+ opts = opts || {};
179
+ var selfWindow = opts.selfWindow || (typeof window !== 'undefined' ? window : null);
180
+ if (!targetWindow) {
181
+ throw new Error('[shim/rpc] makeWindowPostMessageChannel requires targetWindow. (FAIL-LOUD.)');
182
+ }
183
+ if (!selfWindow) {
184
+ throw new Error('[shim/rpc] makeWindowPostMessageChannel requires a self window (pass ' +
185
+ 'opts.selfWindow outside a browser). (FAIL-LOUD.)');
186
+ }
187
+ return {
188
+ post: function (msg) { targetWindow.postMessage(msg, '*'); },
189
+ subscribe: function (handler) {
190
+ var listener = function (ev) {
191
+ if (ev.source !== targetWindow) { return; }
192
+ handler(ev.data);
193
+ };
194
+ selfWindow.addEventListener('message', listener);
195
+ return function () { selfWindow.removeEventListener('message', listener); };
196
+ }
197
+ };
198
+ }
199
+
200
+ // ── Export ────────────────────────────────────────────────────────────────────
201
+
202
+ export {
203
+ makeInAppRpc,
204
+ makeChannelRpc,
205
+ installRpcResponder,
206
+ makeWindowPostMessageChannel
207
+ };