@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.
- package/CLAUDE.md +87 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +212 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +692 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +326 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +672 -0
- package/dist/shim/notebook/knob.js +151 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +510 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +214 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +807 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +62 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module realm-transport
|
|
5
|
+
* @description
|
|
6
|
+
* Transport abstraction a realm pre-load's `boot(hostEl, transport)` receives.
|
|
7
|
+
*
|
|
8
|
+
* ONE transport, **in-app**: same-origin direct scripting. The descriptor may ignore
|
|
9
|
+
* `post`/`subscribe` and script the realm env directly. The transport is a
|
|
10
|
+
* `kind:'in-app'` marker + an in-memory pub/sub channel.
|
|
11
|
+
*
|
|
12
|
+
* The transport object shape a descriptor's `boot()` receives:
|
|
13
|
+
* ```js
|
|
14
|
+
* {
|
|
15
|
+
* kind: 'in-app',
|
|
16
|
+
* post(message): void, // send a control/data message
|
|
17
|
+
* subscribe(handler): unsubFn, // receive messages from the realm
|
|
18
|
+
* isInApp(): boolean // convenience check
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Cell code NEVER branches on transport — it sees only the frozen `ViewHandle`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} RealmTransport
|
|
27
|
+
* @property {'in-app'} kind — transport mode.
|
|
28
|
+
* @property {(message: object) => void} post — send a message across the seam.
|
|
29
|
+
* @property {(handler: Function) => Function} subscribe
|
|
30
|
+
* Register a message handler; returns an unsubscribe function.
|
|
31
|
+
* @property {() => boolean} isInApp — returns `true` for in-app transports.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create the in-app (same-origin) transport.
|
|
38
|
+
*
|
|
39
|
+
* `post`/`subscribe` are a trivial in-memory pub/sub channel so a descriptor
|
|
40
|
+
* written transport-agnostically still works. A descriptor that scripts its
|
|
41
|
+
* env directly may call `isInApp()` and skip them.
|
|
42
|
+
*
|
|
43
|
+
* @returns {RealmTransport}
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* import { inAppTransport } from 'lib/shim/notebook/realm-transport.js';
|
|
47
|
+
* const transport = inAppTransport();
|
|
48
|
+
* const { handle } = myDescriptor.boot(hostEl, transport);
|
|
49
|
+
*/
|
|
50
|
+
export function inAppTransport() {
|
|
51
|
+
var handlers = [];
|
|
52
|
+
return {
|
|
53
|
+
kind: 'in-app',
|
|
54
|
+
isInApp: function () { return true; },
|
|
55
|
+
post: function (message) {
|
|
56
|
+
// Direct dispatch — same window, synchronous. (A descriptor that scripts
|
|
57
|
+
// its env directly need not use this.)
|
|
58
|
+
for (var i = 0; i < handlers.length; i++) {
|
|
59
|
+
try { handlers[i](message); } catch (e) { /* observer error is not the sender's */ }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
subscribe: function (handler) {
|
|
63
|
+
if (typeof handler !== 'function') {
|
|
64
|
+
throw new Error('[realm-transport:in-app] subscribe requires a function.');
|
|
65
|
+
}
|
|
66
|
+
handlers.push(handler);
|
|
67
|
+
return function () {
|
|
68
|
+
var idx = handlers.indexOf(handler);
|
|
69
|
+
if (idx !== -1) { handlers.splice(idx, 1); }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* setup-builder — the SOLE authority that synthesizes the canonical experiment
|
|
5
|
+
* BUILDER source from a `canvas` cell's declarative configuration
|
|
6
|
+
* (WS-NOTEBOOK-SHIM-MANAGED-BUILDER).
|
|
7
|
+
*
|
|
8
|
+
* Named ESM export. PURE module — NO DOM / engine / Runtime / Bus import (C8). It
|
|
9
|
+
* is host-neutral and node-testable: hosts (the notebook-pane in-app) import this
|
|
10
|
+
* one helper and NONE inlines the template (the §router single-path-authority rule applied to the
|
|
11
|
+
* setup-builder template).
|
|
12
|
+
*
|
|
13
|
+
* ── What it does ─────────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* Given a canvas declaration — `setup="<module-name>#<exportName>"` for a factory canvas,
|
|
16
|
+
* `setup="<module-name>"` for a recipe canvas (+ `opts`,
|
|
17
|
+
* `stage`, `seed`, `renderOptions`, `cameraPreset`) — and the notebook's user-code
|
|
18
|
+
* module SET (`{ name → source }`), it returns the canonical builder SOURCE STRING
|
|
19
|
+
* + the realm-internal output name:
|
|
20
|
+
*
|
|
21
|
+
* const { frqtl } = caps;
|
|
22
|
+
* await frqtl.ensureEngine();
|
|
23
|
+
* const { <export> } = await import('<module>');
|
|
24
|
+
* const opts = frqtl.knob('<export>', <opts>);
|
|
25
|
+
* const runner = frqtl.FrqtlApp({ canvasW:<W>, canvasH:<H>, seed:<seed> });
|
|
26
|
+
* runner.setup(frqtl.carryFactoryStatics((u, o) => <export>(frqtl, u, o), <export>), opts);
|
|
27
|
+
* runner.setRenderOption('<k>', <v>); // one line per renderOptions key (fix-loop #6)
|
|
28
|
+
* if (runner.cameraControl && …setPreset) { … } // ONLY when cameraPreset is declared (fix-loop #6)
|
|
29
|
+
* return { <outName>: runner };
|
|
30
|
+
*
|
|
31
|
+
* The synthesized source is fed VERBATIM to the EXISTING `runBuilderThenMount` →
|
|
32
|
+
* `Runtime.runCell` → `_mountRunnerInProcess` seam — ONE executor, ONE
|
|
33
|
+
* source-origin. The retired authored builder body was the previous source-origin;
|
|
34
|
+
* `mode:'declared'` synthesis is now the SOLE one. The runner is realm-internal —
|
|
35
|
+
* `<outName>` is an internal binding the synthesized cell returns, NOT a kernel
|
|
36
|
+
* binding the user references.
|
|
37
|
+
*
|
|
38
|
+
* ── C5: the `setup` grammar, by shape (parseSetupDeclaration — the ONE parser) ─
|
|
39
|
+
*
|
|
40
|
+
* shape="factory": `setup="<module>#<factoryName>"` — a setup module exports its factory BY NAME;
|
|
41
|
+
* the `#name` is REQUIRED and must be one of the module's exports.
|
|
42
|
+
* shape="recipe": `setup="<module>"` — the recipe is the module's DEFAULT export
|
|
43
|
+
* (`export default function buildExperiment(exp)`); a `#name` is REFUSED.
|
|
44
|
+
* Either violation THROWS naming the valid form. P5 deletes shape="factory" and this branch with it.
|
|
45
|
+
*
|
|
46
|
+
* ── C1: the import resolves against the in-memory module closure ──────────────
|
|
47
|
+
*
|
|
48
|
+
* The emitted `import()` uses the DECLARED module name (e.g. `'./setups/wave.js'`),
|
|
49
|
+
* which `runBuilderThenMount` resolves against the notebook's `moduleSet()` via the
|
|
50
|
+
* existing in-memory blob closure (`module-resolver.js`). An unknown/missing module
|
|
51
|
+
* → FAIL-LOUD HERE (we require the module to be present in the module set) so the
|
|
52
|
+
* caller never ships a builder that 404s the relative import downstream.
|
|
53
|
+
*
|
|
54
|
+
* ── C14: the module import is DYNAMIC and runs AFTER ensureEngine (ordering
|
|
55
|
+
* guarantee, WS gym-controls-as-sugar Bite 0a REMEDIATION, Finding 2) ─────────
|
|
56
|
+
*
|
|
57
|
+
* A canonical setup factory's browser-classic export branch captures window.*
|
|
58
|
+
* game-layer globals (LargeObject, SlitScreen, PointSource, …) into closure vars
|
|
59
|
+
* AT MODULE-EVALUATION TIME — not at call time. Under a STATIC top-level
|
|
60
|
+
* `import`, the ENTIRE reachable module graph (this builder → the notebook
|
|
61
|
+
* module cell → any bare-specifier canonical factory it imports) is resolved
|
|
62
|
+
* and evaluated as part of instantiating the builder's OWN module, which
|
|
63
|
+
* happens BEFORE this function body runs — i.e. BEFORE `ensureEngine()`, which
|
|
64
|
+
* is what actually boots the game layer. That raced the game-layer boot
|
|
65
|
+
* (observed: an intermittent ~15% failure that passed only when a PRIOR
|
|
66
|
+
* cell/page action had already booted the engine). A DYNAMIC `await
|
|
67
|
+
* import(...)` defers resolution+evaluation of that ENTIRE graph to the exact
|
|
68
|
+
* point of the call; placing it after the awaited `ensureEngine()` turns the
|
|
69
|
+
* ordering into a deterministic GUARANTEE. `compileEntryModuleSource` +
|
|
70
|
+
* `rewriteRelativeToSiblingBlobs` rewrite a relative specifier inside a
|
|
71
|
+
* dynamic `import(...)` call identically to a static header import (see
|
|
72
|
+
* module-resolver.js) — the blob-closure rewrite mechanism itself did not need
|
|
73
|
+
* to change, only WHERE the import textually lives.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
'use strict';
|
|
77
|
+
|
|
78
|
+
import { canonKey } from './module-resolver.js';
|
|
79
|
+
|
|
80
|
+
/* ── parse "<module-name>#<exportName>" ──────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
// Split the setup declaration into { moduleName, exportName } under its shape's grammar (C5): a
|
|
83
|
+
// factory canvas REQUIRES "#<factoryName>", a recipe canvas REFUSES any "#". The exportName is
|
|
84
|
+
// the substring after the FIRST '#'; absent → ''. The moduleName is verbatim
|
|
85
|
+
// (kept with its relative './…' prefix so the emitted import matches the module
|
|
86
|
+
// cell name the closure is keyed by).
|
|
87
|
+
export function parseSetupDeclaration(setup, shape) {
|
|
88
|
+
if (shape !== 'factory' && shape !== 'recipe') {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'[setup-builder] parseSetupDeclaration(setup, shape): shape is "factory" or "recipe" (got ' +
|
|
91
|
+
JSON.stringify(shape) + ') — the `setup` grammar depends on it. (FAIL-LOUD.)'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (typeof setup !== 'string' || !setup.trim()) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'[setup-builder] a declared canvas requires a `setup` attribute — ' +
|
|
97
|
+
(shape === 'recipe' ? '`setup="<module-name>"`' : '`setup="<module-name>#<factoryName>"`') +
|
|
98
|
+
' (got: ' + JSON.stringify(setup) + '). (FAIL-LOUD.)'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
var s = setup.trim();
|
|
102
|
+
var hash = s.indexOf('#');
|
|
103
|
+
var moduleName = (hash === -1) ? s : s.slice(0, hash);
|
|
104
|
+
var exportName = (hash === -1) ? '' : s.slice(hash + 1);
|
|
105
|
+
moduleName = moduleName.trim();
|
|
106
|
+
exportName = exportName.trim();
|
|
107
|
+
if (!moduleName) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'[setup-builder] `setup` declaration "' + setup + '" has no module name ' +
|
|
110
|
+
'before the "#". Expected "<module-name>#<exportName>". (FAIL-LOUD.)'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (shape === 'recipe' && hash !== -1) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
'[setup-builder] a recipe canvas names no export: `setup="' + setup + '"` carries "#' + exportName +
|
|
116
|
+
'", but a recipe is the module\'s DEFAULT export (`export default function buildExperiment(exp)`) — ' +
|
|
117
|
+
'write setup="' + moduleName + '". (FAIL-LOUD.)'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (shape === 'factory' && !exportName) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'[setup-builder] a factory canvas names its factory: `setup="' + setup + '"` has no "#<factoryName>" — ' +
|
|
123
|
+
'a setup module exports its factory by name; write setup="' + moduleName + '#<factoryName>". (FAIL-LOUD.)'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return { moduleName: moduleName, exportName: exportName };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ── enumerate a module's top-level exports (C5) ─────────────────────────────── */
|
|
130
|
+
|
|
131
|
+
// Return the array of top-level export NAMES declared in an ES-module source. Used
|
|
132
|
+
// ONLY to resolve a single-export module when `#export` is omitted (C5). Strips
|
|
133
|
+
// block + line comments so prose like "// export function" is not counted. This is
|
|
134
|
+
// a deliberately small, conservative scan: it recognizes the export forms the
|
|
135
|
+
// notebook setups use — `export function f`, `export const/let/var f`, and
|
|
136
|
+
// `export { a, b as c }`. `export default` is NOT a named setup export (it has no
|
|
137
|
+
// name to import by) and is excluded.
|
|
138
|
+
export function moduleExportNames(source) {
|
|
139
|
+
var src = String(source == null ? '' : source)
|
|
140
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
141
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, '$1');
|
|
142
|
+
var names = [];
|
|
143
|
+
var re = /\bexport\s+(?:async\s+)?(?:function\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/g;
|
|
144
|
+
var m;
|
|
145
|
+
while ((m = re.exec(src)) !== null) { names.push(m[1]); }
|
|
146
|
+
// `export { a, b as c }` — the EXTERNALLY-visible name is the post-`as` name.
|
|
147
|
+
var braceRe = /\bexport\s*\{([^}]*)\}/g;
|
|
148
|
+
var bm;
|
|
149
|
+
while ((bm = braceRe.exec(src)) !== null) {
|
|
150
|
+
var inner = bm[1];
|
|
151
|
+
inner.split(',').forEach(function (part) {
|
|
152
|
+
var t = part.trim();
|
|
153
|
+
if (!t) { return; }
|
|
154
|
+
var asM = /\bas\s+([A-Za-z_$][\w$]*)\s*$/.exec(t);
|
|
155
|
+
if (asM) { names.push(asM[1]); return; }
|
|
156
|
+
var id = /^([A-Za-z_$][\w$]*)$/.exec(t);
|
|
157
|
+
if (id) { names.push(id[1]); }
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// De-dup, preserve first-seen order.
|
|
161
|
+
var seen = Object.create(null);
|
|
162
|
+
var out = [];
|
|
163
|
+
for (var i = 0; i < names.length; i++) {
|
|
164
|
+
if (names[i] === 'default') { continue; }
|
|
165
|
+
if (!seen[names[i]]) { seen[names[i]] = true; out.push(names[i]); }
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ── resolve the module source from the module set (C1) ──────────────────────── */
|
|
171
|
+
|
|
172
|
+
// Look the declared module up in the module set, matching by canonical key so
|
|
173
|
+
// './setups/x.js' and 'setups/x.js' resolve the same. FAIL-LOUD on absence.
|
|
174
|
+
function _resolveModuleSource(moduleName, modules) {
|
|
175
|
+
var set = modules || {};
|
|
176
|
+
if (Object.prototype.hasOwnProperty.call(set, moduleName)) { return set[moduleName]; }
|
|
177
|
+
var wantKey = canonKey(moduleName);
|
|
178
|
+
var keys = Object.keys(set);
|
|
179
|
+
for (var i = 0; i < keys.length; i++) {
|
|
180
|
+
if (canonKey(keys[i]) === wantKey) { return set[keys[i]]; }
|
|
181
|
+
}
|
|
182
|
+
throw new Error(
|
|
183
|
+
'[setup-builder] declared setup module "' + moduleName + '" is not a ' +
|
|
184
|
+
'`type="module"` cell in this notebook (known modules: [' + keys.join(', ') +
|
|
185
|
+
']). (FAIL-LOUD — the declared canvas references an editable in-notebook module ' +
|
|
186
|
+
'cell, never an opaque by-name load.)'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ── resolve the export name (C5) ────────────────────────────────────────────── */
|
|
191
|
+
|
|
192
|
+
// Return the factory export to import: the declared `exportName` (parseSetupDeclaration requires
|
|
193
|
+
// one on a factory canvas), verified to be one of the module's exports.
|
|
194
|
+
function _resolveExportName(moduleName, exportName, source) {
|
|
195
|
+
var exports = moduleExportNames(source);
|
|
196
|
+
if (exports.indexOf(exportName) === -1) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
'[setup-builder] declared setup export "' + exportName + '" is not exported ' +
|
|
199
|
+
'by module "' + moduleName + '" (its exports: [' + exports.join(', ') +
|
|
200
|
+
']). (FAIL-LOUD.)'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return exportName;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ── stage parsing (C4) ──────────────────────────────────────────────────────── */
|
|
207
|
+
|
|
208
|
+
// Parse the `stage="<W>x<H>"` attr → { canvasW, canvasH } or null when absent. A
|
|
209
|
+
// malformed stage (not <int>x<int>) → FAIL-LOUD (no silent default that hides a typo).
|
|
210
|
+
export function parseStage(stage) {
|
|
211
|
+
if (stage == null || stage === '') { return null; }
|
|
212
|
+
var m = /^(\d+)\s*[x×]\s*(\d+)$/.exec(String(stage).trim());
|
|
213
|
+
if (!m) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
'[setup-builder] `stage` must be "<W>x<H>" (e.g. "480x480"); got ' +
|
|
216
|
+
JSON.stringify(stage) + '. (FAIL-LOUD.)'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return { canvasW: parseInt(m[1], 10), canvasH: parseInt(m[2], 10) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Parse the `seed="<N>"` attr → an integer or null when absent. A non-integer seed
|
|
223
|
+
// → FAIL-LOUD.
|
|
224
|
+
export function parseSeed(seed) {
|
|
225
|
+
if (seed == null || seed === '') { return null; }
|
|
226
|
+
var n = Number(String(seed).trim());
|
|
227
|
+
if (!Number.isInteger(n)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
'[setup-builder] `seed` must be an integer; got ' + JSON.stringify(seed) +
|
|
230
|
+
'. (FAIL-LOUD.)'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return n;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── the synthesis (SOLE authority) ──────────────────────────────────────────── */
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* synthesizeBuilderSource — turn a declared canvas configuration into the canonical
|
|
240
|
+
* BUILDER source string + the realm-internal output name.
|
|
241
|
+
*
|
|
242
|
+
* @param {object} decl
|
|
243
|
+
* @param {string} decl.setup — `"<module-name>#<exportName>"` (exportName optional, C5).
|
|
244
|
+
* @param {Object<string,string>} decl.modules — the notebook module set `{ name → source }`,
|
|
245
|
+
* used to resolve a single export + verify the module exists.
|
|
246
|
+
* @param {Object<string,number|string>} [decl.opts] — the driving-knob map (validated by
|
|
247
|
+
* `frqtl.knob` at run time; `{}` → `frqtl.knob` returns `{}`).
|
|
248
|
+
* @param {string} [decl.stage] — `"<W>x<H>"` runner stage (C4): REQUIRED for a factory canvas
|
|
249
|
+
* (the runner has no default size), REFUSED for a recipe canvas (the recipe states its world).
|
|
250
|
+
* @param {boolean} [decl.render3d] — the cell's display declaration: a factory canvas's runner is
|
|
251
|
+
* built 3D (`is3D: true`); a recipe canvas states 3D itself (`exp.createScene`).
|
|
252
|
+
* @param {string|number} [decl.seed] — runner RNG seed (C4); empty → FrqtlApp default.
|
|
253
|
+
* @param {Object<string,number|string>} [decl.renderOptions] — WS gym-controls-as-sugar
|
|
254
|
+
* fix-loop #6 Finding B: render-INTENT key/value pairs applied via
|
|
255
|
+
* `runner.setRenderOption(key, value)` calls, ONE per key, emitted AFTER
|
|
256
|
+
* `runner.setup(...)` — mirroring the proven page-side order
|
|
257
|
+
* (`fx-experiment.js`: setup → setRenderOption(renderDefaults) → cameraControl).
|
|
258
|
+
* A SEPARATE channel from `decl.opts` (never merged — `opts` are physics-factory
|
|
259
|
+
* arguments; `renderOptions` are renderer-property-bag writes). `{}` → no calls
|
|
260
|
+
* emitted (byte-identical to a canvas with no `renderOptions` attr).
|
|
261
|
+
* @param {string} [decl.cameraPreset] — WS gym-controls-as-sugar fix-loop #6 Finding B:
|
|
262
|
+
* a named camera-orbit preset applied via the CAPABILITY-CHECKED
|
|
263
|
+
* `runner.cameraControl && runner.cameraControl.setPreset(name)` (skipped when the active
|
|
264
|
+
* renderer has no camera control — the page-side `if (cam) { cam.setPreset(...) }` idiom;
|
|
265
|
+
* a camera that does not offer the preset THROWS). Empty/absent → no call emitted.
|
|
266
|
+
* @param {string} [decl.shape] — the setup-form discriminator (Bite 5c): `'factory'`
|
|
267
|
+
* (DEFAULT when empty/absent) or `'recipe'`. Any OTHER value → FAIL-LOUD (never a
|
|
268
|
+
* silent fallback). See the two-form note below.
|
|
269
|
+
* @param {string} [decl.outName] — realm-internal output name; default `'_run'`.
|
|
270
|
+
* @returns {{ builderCode: string, outName: string, moduleName: string, exportName: string, usesModulePath: true, shape: string }}
|
|
271
|
+
* `usesModulePath` is ALWAYS `true` — see C15 below; the caller MUST forward it to
|
|
272
|
+
* `Runtime.runCell({ forceModulePath: synth.usesModulePath, … })`.
|
|
273
|
+
* @throws {Error} FAIL-LOUD on a missing module / unknown export / zero-or-multiple
|
|
274
|
+
* exports with no `#export` / malformed stage|seed / an unknown `shape`.
|
|
275
|
+
*
|
|
276
|
+
* ── Two setup forms — the `shape` discriminator (Bite 5c) ─────────────────────
|
|
277
|
+
*
|
|
278
|
+
* A declared canvas's `setup` module takes ONE of two shapes; the CELL_ATTR `shape`
|
|
279
|
+
* (owner: lib/shim/notebook/format.js) declares which, and this authority synthesizes
|
|
280
|
+
* the matching builder. FAIL-LOUD on an unknown shape — never a silent guess.
|
|
281
|
+
*
|
|
282
|
+
* shape='factory' (DEFAULT when empty): the export is a setup FACTORY
|
|
283
|
+
* `fn(frqtl, universe, options)`. Template:
|
|
284
|
+
* const { <export> } = await import('<module>');
|
|
285
|
+
* const opts = frqtl.knob('<export>', <opts>);
|
|
286
|
+
* const runner = frqtl.FrqtlApp(<cfg>);
|
|
287
|
+
* runner.setup(frqtl.carryFactoryStatics((universe, o) => <export>(frqtl, universe, o), <export>), opts);
|
|
288
|
+
* The arrow carries the export's factory statics (FrqtlApp.FACTORY_STATICS), so the
|
|
289
|
+
* canvas boots the same scene as every other surface.
|
|
290
|
+
*
|
|
291
|
+
* shape='recipe': the module's DEFAULT export is a `buildExperiment(exp)` RECIPE — a SINGLE
|
|
292
|
+
* RecipeContext arg (the sketch form codegen and the scene files write). Template:
|
|
293
|
+
* const buildExperiment = (await import('<module>')).default; // FAIL-LOUD if not a function
|
|
294
|
+
* const exp = frqtl.RecipeContext({ seed, deferCanvas: true }); // SIZELESS
|
|
295
|
+
* exp.build(buildExperiment); // runs its co-init call + addObject() — NO start
|
|
296
|
+
* const runner = exp.runner; // the mount seam owns createCanvas()+start()
|
|
297
|
+
* `deferCanvas:true` lets the notebook realm's `_mountRunnerInProcess` seam own
|
|
298
|
+
* the single createCanvas() (it re-parents to the pane's canvas-wrapper). The
|
|
299
|
+
* recipe takes NO knob bag — a non-empty `opts` on a recipe canvas FAILS LOUD
|
|
300
|
+
* (rather than silently dropping it).
|
|
301
|
+
*
|
|
302
|
+
* WHY the template checks `buildExperiment` but not `exp` (FRAQTL #73): the two values
|
|
303
|
+
* have different OWNERS. `buildExperiment` is read off the module namespace by this template
|
|
304
|
+
* itself, so nothing but this template can vouch for it — hence the emitted FAIL-LOUD check.
|
|
305
|
+
* `exp` comes from `frqtl.RecipeContext(...)`, a caps.frqtl member: its owner
|
|
306
|
+
* (lib/shim/runtime/index.js `_sugar`) FAILS LOUD naming the missing game-layer global,
|
|
307
|
+
* so `exp` is never null here and a second check would re-derive the owner's guarantee
|
|
308
|
+
* for one call site while leaving every other member (runner, Universe, the sugar) to
|
|
309
|
+
* the owner anyway. `RecipeContext` used to return null instead — that null reached
|
|
310
|
+
* `exp.build` as an opaque property-access TypeError, which is what #73 reported.
|
|
311
|
+
*
|
|
312
|
+
* ── C15: the caller MUST force the runtime's cell-as-module path (WS
|
|
313
|
+
* gym-controls-as-sugar Bite 0a fix-loop #3 — tester regression finding
|
|
314
|
+
* 2026-07-03) ──────────────────────────────────────────────────────────────
|
|
315
|
+
*
|
|
316
|
+
* `Runtime.runCell` decides JS-vs-module-blob routing by SNIFFING the cell body
|
|
317
|
+
* for a STATIC top-level `import` (`lib/shim/notebook/module-resolver.js`
|
|
318
|
+
* `hasImport`, `/(^|\n)\s*import\s/`). Since C14 moved this template's module
|
|
319
|
+
* reference to a DYNAMIC `await import(...)` INSIDE the function body (never at
|
|
320
|
+
* line-start), that sniff returns `false` — the runtime took the `new Function`
|
|
321
|
+
* fast path, which never runs `rewriteRelativeToSiblingBlobs`, so the relative
|
|
322
|
+
* specifier survived unrewritten into a dynamic import evaluated from the
|
|
323
|
+
* RUNTIME's own module URL → 404 on every notebook (regression, tester-caught).
|
|
324
|
+
*
|
|
325
|
+
* Widening the sniff regex to detect `import(` ANYWHERE was REJECTED: `import(`
|
|
326
|
+
* can appear inside an ordinary string/comment in an UNRELATED cell (e.g. a
|
|
327
|
+
* math-lane cell's docstring demonstrating dynamic-import syntax) — a broadened
|
|
328
|
+
* sniff would route that cell through `_runCellAsModule`, whose
|
|
329
|
+
* `rewriteRelativeToSiblingBlobs` would then try to treat the coincidental
|
|
330
|
+
* substring as a REAL relative specifier: either a spurious FAIL-LOUD throw (not
|
|
331
|
+
* found in the module set) or, worse, a silent rewrite THAT MANGLES THE STRING
|
|
332
|
+
* LITERAL'S TEXT (if a same-named sibling module happens to exist). That is a
|
|
333
|
+
* genuine correctness hazard, not a benign inefficiency — verified, not assumed.
|
|
334
|
+
*
|
|
335
|
+
* THE FIX: this authority — which KNOWS with certainty its own template always
|
|
336
|
+
* needs the module-blob path, because it just synthesized the dynamic import in
|
|
337
|
+
* question — DECLARES that fact explicitly (`usesModulePath: true`) rather than
|
|
338
|
+
* having the runtime re-derive it via regex on text it did not author with full
|
|
339
|
+
* context. The runtime's `hasImport` sniff is UNCHANGED (still correct for
|
|
340
|
+
* hand-authored module cells with real static imports, and for the sibling
|
|
341
|
+
* `view-builder.js` template, which still uses a static import); ONLY the
|
|
342
|
+
* synthesized-builder call site is exempted from the sniff, by explicit
|
|
343
|
+
* authority signal — never a widened heuristic. See
|
|
344
|
+
* `lib/shim/runtime/index.js` `RunCellSpec.forceModulePath`.
|
|
345
|
+
*/
|
|
346
|
+
export function synthesizeBuilderSource(decl) {
|
|
347
|
+
decl = decl || {};
|
|
348
|
+
// Bite 5c: the setup-form discriminator. Empty/absent → 'factory' (the pre-existing
|
|
349
|
+
// gallery form, byte-identical). 'recipe' → RecipeContext/buildExperiment. Any other
|
|
350
|
+
// value FAILS LOUD — never a silent guess.
|
|
351
|
+
var shape = (decl.shape == null || decl.shape === '') ? 'factory' : String(decl.shape).trim();
|
|
352
|
+
if (shape !== 'factory' && shape !== 'recipe') {
|
|
353
|
+
throw new Error(
|
|
354
|
+
'[setup-builder] unknown canvas `shape` "' + decl.shape + '" — expected ' +
|
|
355
|
+
'"factory" (a `fn(frqtl, universe, options)` setup export) or "recipe" (a ' +
|
|
356
|
+
'`buildExperiment(exp)` RecipeContext recipe). (FAIL-LOUD — never guess.)'
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
var parsed = parseSetupDeclaration(decl.setup, shape);
|
|
360
|
+
var source = _resolveModuleSource(parsed.moduleName, decl.modules);
|
|
361
|
+
|
|
362
|
+
var stage = parseStage(decl.stage);
|
|
363
|
+
var seed = parseSeed(decl.seed);
|
|
364
|
+
// The realm-internal output name is emitted as an UNQUOTED object key in the
|
|
365
|
+
// synthesized `return { <outName>: runner }`. Callers pass `cell.id + '_run'`,
|
|
366
|
+
// and notebook cell ids contain hyphens (e.g. `c-ds-canvas`) — a bare hyphen in
|
|
367
|
+
// a key is a SyntaxError ("Unexpected token '-'"). Sanitize to a valid JS
|
|
368
|
+
// identifier (it is realm-internal, never user-referenced, so the exact spelling
|
|
369
|
+
// does not matter — only that it is a legal key and that the value we RETURN as
|
|
370
|
+
// `outName` matches the key the template emits, so the caller's
|
|
371
|
+
// `result.outputs[outName]` lookup hits).
|
|
372
|
+
var rawOutName = decl.outName || '_run';
|
|
373
|
+
var outName = String(rawOutName).replace(/[^A-Za-z0-9_$]/g, '_');
|
|
374
|
+
if (/^[0-9]/.test(outName)) { outName = '_' + outName; }
|
|
375
|
+
var optsLiteral = JSON.stringify(decl.opts || {});
|
|
376
|
+
|
|
377
|
+
// Build the FrqtlApp config object literal from the DECLARED config only
|
|
378
|
+
// (C4: stage/seed are the runner config authority, never folded into opts). An
|
|
379
|
+
// undeclared seed takes the FrqtlApp default; a size is never defaulted (above).
|
|
380
|
+
var cfgParts = [];
|
|
381
|
+
if (stage) {
|
|
382
|
+
cfgParts.push('canvasW: ' + stage.canvasW);
|
|
383
|
+
cfgParts.push('canvasH: ' + stage.canvasH);
|
|
384
|
+
}
|
|
385
|
+
if (seed !== null) { cfgParts.push('seed: ' + seed); }
|
|
386
|
+
// A `render3d` canvas boots the three display, which IS the 3D display: the factory's runner
|
|
387
|
+
// is told so at construction (the provider's perspective camera), from the SAME cell attr
|
|
388
|
+
// that chose the display — never a global.
|
|
389
|
+
var factoryCfgParts = decl.render3d ? cfgParts.concat(['is3D: true']) : cfgParts;
|
|
390
|
+
var cfgLiteral = '{ ' + factoryCfgParts.join(', ') + ' }';
|
|
391
|
+
|
|
392
|
+
// WS gym-controls-as-sugar fix-loop #6 Finding B: render-INTENT lines, emitted
|
|
393
|
+
// AFTER runner.setup() (mirrors the proven page-side order in fx-experiment.js:
|
|
394
|
+
// setup() → setRenderOption(renderDefaults) → cameraControl.setPreset()). Each
|
|
395
|
+
// `renderOptions` key becomes its OWN `runner.setRenderOption('<k>', <v>);` call
|
|
396
|
+
// — a SEPARATE channel from the `opts` physics-knob bag above (never merged;
|
|
397
|
+
// see the JSDoc). `{}` / absent → zero lines (byte-identical to before this
|
|
398
|
+
// fix-loop for every existing canvas that doesn't declare `renderOptions`).
|
|
399
|
+
var renderOptionLines = '';
|
|
400
|
+
var renderOptions = decl.renderOptions || {};
|
|
401
|
+
Object.keys(renderOptions).forEach(function (k) {
|
|
402
|
+
renderOptionLines += 'runner.setRenderOption(' + JSON.stringify(k) + ', ' +
|
|
403
|
+
JSON.stringify(renderOptions[k]) + ');\n';
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// cameraPreset: a CAPABILITY-OBJECT method call (runner.cameraControl), not a
|
|
407
|
+
// property-bag write — a THIRD channel from both opts and renderOptions (see
|
|
408
|
+
// JSDoc). Capability-checked exactly like the page-side `if (cam) {...}` idiom
|
|
409
|
+
// — skipped only with no camera control at all; a camera that does not offer the
|
|
410
|
+
// preset THROWS (its own setPreset), never a silent no-op.
|
|
411
|
+
var cameraPresetLine = '';
|
|
412
|
+
if (decl.cameraPreset) {
|
|
413
|
+
cameraPresetLine =
|
|
414
|
+
"if (runner.cameraControl && typeof runner.cameraControl.setPreset === 'function') {\n" +
|
|
415
|
+
" runner.cameraControl.setPreset(" + JSON.stringify(decl.cameraPreset) + ");\n" +
|
|
416
|
+
"}\n";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// The canonical template (the §0 builder shape). Both shapes:
|
|
420
|
+
// - boot the engine FIRST (ensureEngine),
|
|
421
|
+
// - THEN dynamically import the editable in-notebook module by name (C1) — WS
|
|
422
|
+
// gym-controls-as-sugar Bite 0a REMEDIATION, Finding 2: a canonical setup
|
|
423
|
+
// factory's browser-classic branch captures window.* game-layer globals
|
|
424
|
+
// (LargeObject, SlitScreen, …) into closure vars AT MODULE-EVALUATION TIME. A
|
|
425
|
+
// STATIC top-level `import` of the module cell is resolved+evaluated as part
|
|
426
|
+
// of instantiating THIS builder's own module graph — BEFORE this function body
|
|
427
|
+
// (and therefore BEFORE `ensureEngine()`) ever runs, racing the game-layer
|
|
428
|
+
// boot. A DYNAMIC `import()` defers that to the exact point of the call —
|
|
429
|
+
// placing it AFTER the awaited `ensureEngine()` makes the ordering a GUARANTEE.
|
|
430
|
+
// (compileEntryModuleSource + rewriteRelativeToSiblingBlobs rewrite a relative
|
|
431
|
+
// specifier inside a dynamic `import()` call identically to a static header
|
|
432
|
+
// import — see module-resolver.js.)
|
|
433
|
+
// - populate the runner (factory: runner.setup(...) / recipe: exp.build(...)),
|
|
434
|
+
// - apply the render-intent lines, and return the runner under the outName.
|
|
435
|
+
var exportName;
|
|
436
|
+
var builderCode;
|
|
437
|
+
if (shape === 'recipe') {
|
|
438
|
+
// RECIPE form: the module's DEFAULT export is the `buildExperiment(exp)` recipe.
|
|
439
|
+
exportName = 'default';
|
|
440
|
+
// A recipe takes ONE RecipeContext arg — no knob bag. A non-empty `opts` here
|
|
441
|
+
// would be silently ignored; FAIL LOUD instead (no silent drop).
|
|
442
|
+
if (decl.opts && Object.keys(decl.opts).length > 0) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
'[setup-builder] a recipe-shape canvas ("' + parsed.moduleName +
|
|
445
|
+
'") takes NO `opts` — `buildExperiment(exp)` receives a ' +
|
|
446
|
+
'single RecipeContext, not a knob bag (got opts ' + JSON.stringify(decl.opts) +
|
|
447
|
+
'). Remove the `opts` attr or use shape="factory". (FAIL-LOUD.)'
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
// ONE size statement per world: the recipe states its own in its co-init call.
|
|
451
|
+
if (stage) {
|
|
452
|
+
throw new Error('[setup-builder] a recipe-shape canvas ("' + parsed.moduleName + '") takes NO `stage` — ' +
|
|
453
|
+
'the recipe states its world, size included, in its exp.createCanvas(w, h, cfg) / ' +
|
|
454
|
+
'exp.createScene(w, h, d, cfg) call; a stage here would be a second size. (FAIL-LOUD.)');
|
|
455
|
+
}
|
|
456
|
+
// deferCanvas: true — the notebook realm's `_mountRunnerInProcess` seam owns the
|
|
457
|
+
// single createCanvas()+start() (re-parenting to the pane's canvas-wrapper); an
|
|
458
|
+
// eager RecipeContext-ctor createCanvas would orphan a second canvas.
|
|
459
|
+
var recipeCfgParts = cfgParts.slice();
|
|
460
|
+
recipeCfgParts.push('deferCanvas: true');
|
|
461
|
+
var recipeCfgLiteral = '{ ' + recipeCfgParts.join(', ') + ' }';
|
|
462
|
+
builderCode =
|
|
463
|
+
"const { frqtl } = caps;\n" +
|
|
464
|
+
"await frqtl.ensureEngine();\n" +
|
|
465
|
+
"const buildExperiment = (await import('" + parsed.moduleName + "')).default;\n" +
|
|
466
|
+
"if (typeof buildExperiment !== 'function') {\n" +
|
|
467
|
+
" throw new Error('[setup-builder] recipe module \"" + parsed.moduleName + "\" has no default export function — a recipe-shape canvas runs `export default function buildExperiment(exp)` (a classic `window.buildExperiment` tail defines no export). (FAIL-LOUD.)');\n" +
|
|
468
|
+
"}\n" +
|
|
469
|
+
"const exp = frqtl.RecipeContext(" + recipeCfgLiteral + ");\n" +
|
|
470
|
+
"exp.build(buildExperiment);\n" +
|
|
471
|
+
"const runner = exp.runner;\n" +
|
|
472
|
+
renderOptionLines +
|
|
473
|
+
cameraPresetLine +
|
|
474
|
+
"return { " + outName + ": runner };\n";
|
|
475
|
+
} else {
|
|
476
|
+
// FACTORY form (default): the export is `fn(frqtl, universe, options)`. Resolve
|
|
477
|
+
// the ESM export (C5) and validate the knob bag.
|
|
478
|
+
exportName = _resolveExportName(parsed.moduleName, parsed.exportName, source);
|
|
479
|
+
// A factory takes the runner's world, which only `stage` states — there is no default size.
|
|
480
|
+
if (!stage) {
|
|
481
|
+
throw new Error('[setup-builder] a factory-shape canvas ("' + parsed.moduleName + '") needs ' +
|
|
482
|
+
'`stage="<W>x<H>"` — the runner has no default world size. (FAIL-LOUD.)');
|
|
483
|
+
}
|
|
484
|
+
builderCode =
|
|
485
|
+
"const { frqtl } = caps;\n" +
|
|
486
|
+
"await frqtl.ensureEngine();\n" +
|
|
487
|
+
"const { " + exportName + " } = await import('" + parsed.moduleName + "');\n" +
|
|
488
|
+
"const opts = frqtl.knob('" + exportName + "', " + optsLiteral + ");\n" +
|
|
489
|
+
"const runner = frqtl.FrqtlApp(" + cfgLiteral + ");\n" +
|
|
490
|
+
"runner.setup(frqtl.carryFactoryStatics((universe, o) => " + exportName + "(frqtl, universe, o), " +
|
|
491
|
+
exportName + "), opts);\n" +
|
|
492
|
+
renderOptionLines +
|
|
493
|
+
cameraPresetLine +
|
|
494
|
+
"return { " + outName + ": runner };\n";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
builderCode: builderCode,
|
|
499
|
+
outName: outName,
|
|
500
|
+
moduleName: parsed.moduleName,
|
|
501
|
+
exportName: exportName,
|
|
502
|
+
shape: shape,
|
|
503
|
+
// C15 — ALWAYS true (the template ALWAYS carries a body-level dynamic
|
|
504
|
+
// import by construction). The caller MUST forward this to
|
|
505
|
+
// Runtime.runCell({ forceModulePath: true, … }) — the runtime's own
|
|
506
|
+
// hasImport() text-sniff returns false for this template (the import is
|
|
507
|
+
// not at line-start) and must NOT be relied on for synthesized builders.
|
|
508
|
+
usesModulePath: true
|
|
509
|
+
};
|
|
510
|
+
}
|