@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,227 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* frqtl-control-overlay.js — THE canvas control overlay (presentation pass Item 2).
|
|
5
|
+
*
|
|
6
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* `FrqtlControlOverlay.mount(runner, opts)` →
|
|
9
|
+
* `{ dispose(), refresh(), controls: string[] }`
|
|
10
|
+
*
|
|
11
|
+
* runner — the scene's runner. The overlay mounts through its ONE door,
|
|
12
|
+
* `runner.controlHost` (the canvas wrapper by default; an element a
|
|
13
|
+
* hosting pane owns above its own overlays when one is set — and it
|
|
14
|
+
* MOVES there when a host is set later). Its nodes are created in that
|
|
15
|
+
* host's ownerDocument. A scene never names a DOM id.
|
|
16
|
+
* opts.registry — the SAME `makeControlRegistry()` instance the page passed to
|
|
17
|
+
* `FrqtlControlsPanel.exposeRegistry` (REQUIRED).
|
|
18
|
+
* opts.name — the registry instance name the controls were registered under.
|
|
19
|
+
*
|
|
20
|
+
* Only descriptors carrying a `place: {x,y,anchor}` are drawn (the shim owns that
|
|
21
|
+
* shape). Everything else stays panel-only, so adding an overlay to a page never
|
|
22
|
+
* changes what its panels show.
|
|
23
|
+
*
|
|
24
|
+
* ── One write path, one widget table, one placement authority ───────────────
|
|
25
|
+
*
|
|
26
|
+
* Every write routes through `FrqtlControlsPanel.dispatch` — the exported single
|
|
27
|
+
* dispatch point (this overlay is its FOURTH consumer, after the in-app path, the
|
|
28
|
+
* RPC responder and frqtl-control-sync). There is NO second binding path here:
|
|
29
|
+
* a value write is `setControlValue`, an action is `runControlAction`, exactly as
|
|
30
|
+
* the panel does. Widgets come from the ONE widget table
|
|
31
|
+
* (`frqtl-control-widgets.js`), and live-state reconciliation from
|
|
32
|
+
* `frqtl-control-sync.js` — so a placed toggle's checked state DERIVES from live
|
|
33
|
+
* state (truth-in-labelling) instead of drifting after a change made elsewhere.
|
|
34
|
+
*
|
|
35
|
+
* ── An HTML overlay is the ONE mechanism for BOTH renderers ─────────────────
|
|
36
|
+
*
|
|
37
|
+
* p5's DOM helpers (createButton/createSlider) are deliberately NOT used: a
|
|
38
|
+
* p5-only convenience would be a per-renderer fork of the control surface (three
|
|
39
|
+
* has no equivalent) — i.e. renderer-sniffing above the shim. One absolutely-
|
|
40
|
+
* positioned `<div>` over the canvas parent works identically under both
|
|
41
|
+
* providers and under any BYO renderer, because it never touches the canvas.
|
|
42
|
+
*
|
|
43
|
+
* ── Layer 2: physics behaviour ──────────────────────────────────────────────
|
|
44
|
+
*
|
|
45
|
+
* ZERO physics. The overlay is screen-space DOM over an already-declared control
|
|
46
|
+
* array; driving a control here produces the IDENTICAL simulation effect as
|
|
47
|
+
* driving the same declared control from the page's own slider or the GYM panel
|
|
48
|
+
* (the declare-once oracle).
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { FrqtlControlsPanel as _ControlsPanelNode } from './FrqtlControlsPanel.js';
|
|
52
|
+
import { FrqtlControlWidgets as _ControlWidgetsNode } from '../objects/objects/frqtl-control-widgets.js';
|
|
53
|
+
import { FrqtlControlSync as _ControlSyncNode } from '../objects/objects/frqtl-control-sync.js';
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/** Translate a normalized `place` into absolute CSS on the overlay child. */
|
|
60
|
+
function _applyPlace(el, place) {
|
|
61
|
+
var xPct = (place.x * 100) + '%';
|
|
62
|
+
var yPct = (place.y * 100) + '%';
|
|
63
|
+
var anchor = place.anchor || 'top-left';
|
|
64
|
+
el.style.position = 'absolute';
|
|
65
|
+
if (anchor === 'top-left') { el.style.left = xPct; el.style.top = yPct; }
|
|
66
|
+
if (anchor === 'top-right') { el.style.right = xPct; el.style.top = yPct; }
|
|
67
|
+
if (anchor === 'bottom-left') { el.style.left = xPct; el.style.bottom = yPct; }
|
|
68
|
+
if (anchor === 'bottom-right') { el.style.right = xPct; el.style.bottom = yPct; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* mount(runner, opts) — draw every PLACE-bearing control over the canvas, through
|
|
73
|
+
* `runner.controlHost`.
|
|
74
|
+
* @returns {{dispose:Function, refresh:Function, controls:string[]}}
|
|
75
|
+
* @throws {Error} FAIL-LOUD on a non-runner, a runner with no control host yet, a missing
|
|
76
|
+
* registry or name.
|
|
77
|
+
*/
|
|
78
|
+
function mount(runner, opts) {
|
|
79
|
+
opts = opts || {};
|
|
80
|
+
if (!runner || typeof runner.addControlChrome !== 'function' || !('controlHost' in runner)) {
|
|
81
|
+
throw new Error('[frqtl-control-overlay] mount(runner, opts) takes the scene\'s RUNNER — the overlay ' +
|
|
82
|
+
'mounts through its one door, runner.controlHost (a scene never names a DOM id). (FAIL-LOUD.)');
|
|
83
|
+
}
|
|
84
|
+
var canvasParentEl = runner.controlHost;
|
|
85
|
+
if (!canvasParentEl) {
|
|
86
|
+
throw new Error('[frqtl-control-overlay] runner.controlHost is null — the scene canvas is not mounted ' +
|
|
87
|
+
'and no hosting pane set a host (check runner.controlHost before mounting). (FAIL-LOUD.)');
|
|
88
|
+
}
|
|
89
|
+
if (!opts.registry || typeof opts.registry.getControls !== 'function') {
|
|
90
|
+
throw new Error('[frqtl-control-overlay] mount requires opts.registry — the SAME ' +
|
|
91
|
+
'makeControlRegistry() instance the page exposed. (FAIL-LOUD.)');
|
|
92
|
+
}
|
|
93
|
+
if (!opts.name) {
|
|
94
|
+
throw new Error('[frqtl-control-overlay] mount requires opts.name (the registry instance ' +
|
|
95
|
+
'name the controls were registered under). (FAIL-LOUD.)');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var Panel = _ControlsPanelNode;
|
|
99
|
+
var Widgets = _ControlWidgetsNode;
|
|
100
|
+
var registry = opts.registry;
|
|
101
|
+
var name = opts.name;
|
|
102
|
+
|
|
103
|
+
// The overlay root: pointer-events NONE so the canvas keeps its own mouse/drag
|
|
104
|
+
// handling everywhere except directly on a widget (which re-enables them).
|
|
105
|
+
var doc = canvasParentEl.ownerDocument; // nodes live in the HOST's document
|
|
106
|
+
var root = doc.createElement('div');
|
|
107
|
+
root.className = 'frqtl-control-overlay';
|
|
108
|
+
root.setAttribute('data-role', 'frqtl-control-overlay');
|
|
109
|
+
root.style.cssText = 'position:absolute;pointer-events:none;z-index:12;';
|
|
110
|
+
// The parent must establish a positioning context for the absolute children.
|
|
111
|
+
var prevPos = canvasParentEl.style.position;
|
|
112
|
+
if (!prevPos || prevPos === 'static') { canvasParentEl.style.position = 'relative'; }
|
|
113
|
+
canvasParentEl.appendChild(root);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Size the root to the CANVAS box, not the parent's. `place` is defined as
|
|
117
|
+
* fractions OF THE CANVAS, so the overlay must match the drawing surface — a
|
|
118
|
+
* parent with padding/toolbars is taller, which pushed a y:0.03 control above
|
|
119
|
+
* the canvas top edge (measured in the realm). Falls back to filling the parent
|
|
120
|
+
* when no canvas exists yet (the renderer creates it asynchronously); re-sync
|
|
121
|
+
* happens on resize and on every refresh().
|
|
122
|
+
*/
|
|
123
|
+
function sizeToCanvas() {
|
|
124
|
+
var cv = (typeof canvasParentEl.querySelector === 'function')
|
|
125
|
+
? canvasParentEl.querySelector('canvas') : null;
|
|
126
|
+
if (cv) {
|
|
127
|
+
root.style.left = cv.offsetLeft + 'px';
|
|
128
|
+
root.style.top = cv.offsetTop + 'px';
|
|
129
|
+
root.style.width = cv.offsetWidth + 'px';
|
|
130
|
+
root.style.height = cv.offsetHeight + 'px';
|
|
131
|
+
} else {
|
|
132
|
+
root.style.left = '0'; root.style.top = '0';
|
|
133
|
+
root.style.width = '100%'; root.style.height = '100%';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
sizeToCanvas();
|
|
137
|
+
var onResize = function () { sizeToCanvas(); };
|
|
138
|
+
var view = typeof document !== 'undefined' ? document.defaultView : null;
|
|
139
|
+
if (view) {
|
|
140
|
+
view.addEventListener('resize', onResize);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Live-state reconciliation: a placed widget REFLECTS canonical state (a pause
|
|
144
|
+
// toggle flipped from the page's own button updates here too).
|
|
145
|
+
var sync = _ControlSyncNode.makeControlSync({ name: name, registry: registry, trigger: 'manual' });
|
|
146
|
+
|
|
147
|
+
var mounted = []; // [{ id, widget, cell }]
|
|
148
|
+
|
|
149
|
+
function clear() {
|
|
150
|
+
mounted.forEach(function (m) { m.widget.dispose(); if (m.cell.parentNode) { m.cell.parentNode.removeChild(m.cell); } });
|
|
151
|
+
mounted = [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** (Re)build the overlay from the CURRENT descriptor list. */
|
|
155
|
+
function refresh() {
|
|
156
|
+
clear();
|
|
157
|
+
sizeToCanvas();
|
|
158
|
+
// The wire rows come from the ONE dispatch point (never a direct registry
|
|
159
|
+
// read), so the overlay sees exactly what every other host sees.
|
|
160
|
+
var rows = Panel.dispatch(registry, 'listControls', [name]);
|
|
161
|
+
rows.forEach(function (row) {
|
|
162
|
+
if (!row.place) { return; } // panel-only descriptor — not ours to draw
|
|
163
|
+
var cell = doc.createElement('div');
|
|
164
|
+
cell.className = 'frqtl-control-overlay-cell';
|
|
165
|
+
cell.setAttribute('data-frqtl-overlay-control', row.id);
|
|
166
|
+
cell.style.cssText = 'pointer-events:auto;background:rgba(12,18,28,0.72);border:1px solid #33405a;' +
|
|
167
|
+
'border-radius:4px;padding:2px 8px;backdrop-filter:blur(2px);';
|
|
168
|
+
_applyPlace(cell, row.place);
|
|
169
|
+
root.appendChild(cell);
|
|
170
|
+
var w = Widgets.mountWidget(cell, {
|
|
171
|
+
initial: row,
|
|
172
|
+
onInput: function (v) { return Panel.dispatch(registry, 'setControlValue', [name, row.id, v]); },
|
|
173
|
+
onAction: function () {
|
|
174
|
+
var r = Panel.dispatch(registry, 'runControlAction', [name, row.id]);
|
|
175
|
+
// A run-loop verb can change other controls' live state (a restart
|
|
176
|
+
// resets a paused toggle) — reconcile right after.
|
|
177
|
+
sync.pull();
|
|
178
|
+
return r;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
sync.register(row.id, w);
|
|
182
|
+
mounted.push({ id: row.id, widget: w, cell: cell });
|
|
183
|
+
});
|
|
184
|
+
return mounted.map(function (m) { return m.id; });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var ids = refresh();
|
|
188
|
+
|
|
189
|
+
// Through the door: a host set LATER (a hosting pane) moves this overlay above its overlays.
|
|
190
|
+
var unhost = runner.addControlChrome({
|
|
191
|
+
rehost: function (el) {
|
|
192
|
+
if (el === canvasParentEl) { return; }
|
|
193
|
+
unhost();
|
|
194
|
+
if (root.parentNode) { root.parentNode.removeChild(root); }
|
|
195
|
+
canvasParentEl.style.position = prevPos;
|
|
196
|
+
canvasParentEl = el;
|
|
197
|
+
doc = el.ownerDocument;
|
|
198
|
+
prevPos = el.style.position;
|
|
199
|
+
if (!prevPos || prevPos === 'static') { el.style.position = 'relative'; }
|
|
200
|
+
el.appendChild(root);
|
|
201
|
+
sizeToCanvas();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
controls: ids,
|
|
207
|
+
refresh: refresh,
|
|
208
|
+
/** Pull live state into every placed widget (truth-in-labelling). */
|
|
209
|
+
sync: function () { return sync.pull(); },
|
|
210
|
+
dispose: function () {
|
|
211
|
+
clear();
|
|
212
|
+
sync.dispose();
|
|
213
|
+
if (view) {
|
|
214
|
+
view.removeEventListener('resize', onResize);
|
|
215
|
+
}
|
|
216
|
+
if (root.parentNode) { root.parentNode.removeChild(root); }
|
|
217
|
+
canvasParentEl.style.position = prevPos;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
var FrqtlControlOverlay = { mount: mount };
|
|
225
|
+
|
|
226
|
+
export { FrqtlControlOverlay };
|
|
227
|
+
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* frqtl-controls-panel.js — a placeable host for control collections
|
|
5
|
+
* (WS gym-controls-as-sugar Bite 3 Stage 2, Layer A — the panel itself).
|
|
6
|
+
*
|
|
7
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* `exposeRegistry(registry, opts)` — REALM side. Call this on the experiment
|
|
10
|
+
* page, once, after registering the page's OWN control descriptors into a
|
|
11
|
+
* `makeControlRegistry()` instance (`lib/shim/controls/registry.js`, Bite 1).
|
|
12
|
+
* Makes that registry (a) the page's exposed registry (`exposedRegistry()`, read by a host through
|
|
13
|
+
* the page door `FrqtlPage.controls`) and (b) remotely drivable from a parent frame via `postMessage` (unless
|
|
14
|
+
* `opts.crossFrame === false`).
|
|
15
|
+
*
|
|
16
|
+
* `mount(hostEl, opts)` — PANEL side. Renders one widget row per registered
|
|
17
|
+
* control descriptor into `hostEl`, wired so driving a widget drives the
|
|
18
|
+
* REAL registered control — either in the SAME window (`opts.mode:'in-app'`,
|
|
19
|
+
* `opts.registry` a live registry instance) or across an `<iframe>`
|
|
20
|
+
* (`opts.mode:'iframe'`, `opts.frame` the iframe element). Returns
|
|
21
|
+
* `{ dispose(), ready }`.
|
|
22
|
+
*
|
|
23
|
+
* The panel is a COLLECTION HOST + transport plumbing — it renders no widget
|
|
24
|
+
* itself: each row is handed to `frqtl-control-widgets.js`'s `mountWidget()`,
|
|
25
|
+
* the ONE widget-resolution authority (which itself DELEGATES the slider to
|
|
26
|
+
* `frqtl-control-slider.js`, the ONE slider implementation). The canvas overlay
|
|
27
|
+
* consults the SAME table, so a widget decision is never made twice. The panel
|
|
28
|
+
* supplies only transport-aware callbacks over its OWN handle
|
|
29
|
+
* (`setControlValue` for a value write, `runControlAction` for an action verb)
|
|
30
|
+
* — the widget lib knows nothing about registries, RPC, or transports.
|
|
31
|
+
*
|
|
32
|
+
* `mount` NEVER rebuilds or re-declares the target's controls — it is an
|
|
33
|
+
* ADDITIONAL consumer of whatever was already registered via `exposeRegistry`
|
|
34
|
+
* (the "declare once, consume everywhere" rule). The panel does not know or
|
|
35
|
+
* care whether a descriptor's `onChange` writes through a capability/property
|
|
36
|
+
* accessor (physics channel) or `runner.setRenderOption` (render channel) —
|
|
37
|
+
* that distinction is baked into the descriptor's OWN `onChange` closure by
|
|
38
|
+
* whoever declared it; the panel treats every descriptor uniformly (calls
|
|
39
|
+
* `onChange`), which is exactly how the channel discriminator is "respected"
|
|
40
|
+
* here — as authored metadata, never as a panel-side dispatch branch.
|
|
41
|
+
*
|
|
42
|
+
* ── Transport wiring (the §2 ruling, applied via lib/shim/rpc) ──────────────
|
|
43
|
+
*
|
|
44
|
+
* Same-page (in-app) placement: `ShimRpc.makeInAppRpc(dispatch)` — direct
|
|
45
|
+
* scripting, no postMessage marshaling (both sides are the SAME window).
|
|
46
|
+
* Iframe placement (same- or cross-origin — the mechanism does not care
|
|
47
|
+
* which): `ShimRpc.makeWindowPostMessageChannel` + `ShimRpc.makeChannelRpc`/
|
|
48
|
+
* `installRpcResponder`. BOTH produce the SAME `{call(fn,args)->Promise,
|
|
49
|
+
* dispose()}` shape — the render/wiring code below calls ONLY `handle.call
|
|
50
|
+
* (fn, args)` and NEVER branches on which transport backs it (the
|
|
51
|
+
* serializability discipline stays intact: `listControls`/`setControlValue`'s
|
|
52
|
+
* wire payloads are plain id/label/min/max/step/value/valueText data, never a
|
|
53
|
+
* live closure).
|
|
54
|
+
*
|
|
55
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
56
|
+
*
|
|
57
|
+
* This file adds ZERO physics. It is pure UI plumbing over an
|
|
58
|
+
* ALREADY-DECLARED control array; moving a panel slider produces the
|
|
59
|
+
* IDENTICAL simulation effect as moving the experiment page's own slider for
|
|
60
|
+
* the same control id (proven by the Layer-A Rule-0 oracle: the
|
|
61
|
+
* collision-matrix density/offset/angle sliders, driven from a panel beside
|
|
62
|
+
* the canvas AND from a panel in a separate host page targeting an iframe).
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
import * as _ShimRpcNode from '../../shim/rpc/index.js';
|
|
66
|
+
import { FrqtlControlWidgets as _ControlWidgetsNode } from '../objects/objects/frqtl-control-widgets.js';
|
|
67
|
+
import { ControlBind as _ControlBindNode } from '../objects/objects/controlBind.js';
|
|
68
|
+
import { ControlAction as _ControlActionNode } from '../objects/objects/controlAction.js';
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// Namespaces this panel's RPC traffic so it never collides with another RPC
|
|
73
|
+
// user sharing the same postMessage bus (lib/shim/rpc's opts.tag contract).
|
|
74
|
+
var RPC_TAG = 'frqtl-controls-panel';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The SINGLE dispatch point for a controls-panel verb against a registry —
|
|
78
|
+
* used IDENTICALLY by the in-app direct path and the RPC responder (DRY: one
|
|
79
|
+
* implementation, not two that could drift).
|
|
80
|
+
*
|
|
81
|
+
* Verbs:
|
|
82
|
+
* listControls(name) → Array<{id,label,min,max,step,value,valueText}>
|
|
83
|
+
* setControlValue(name,id,value) → {value, valueText}
|
|
84
|
+
*
|
|
85
|
+
* @param {object} registry — a makeControlRegistry() instance. Bind execution
|
|
86
|
+
* reads its bind context off `registry._controlBindCtx` (attached by
|
|
87
|
+
* `exposeRegistry`'s `opts.universe`/`opts.runner`/`opts.readSnapshot` —
|
|
88
|
+
* WS gym-controls-as-sugar FULL CONVERGENCE build) — attaching it to the
|
|
89
|
+
* registry itself (rather than threading a second parameter through here)
|
|
90
|
+
* means the in-app dispatch path (`_buildInAppHandle`, which is handed this
|
|
91
|
+
* SAME registry instance by `mount({mode:'in-app'})`) sees it for free,
|
|
92
|
+
* with no second signature change needed on `mount`/`_buildInAppHandle`.
|
|
93
|
+
* @param {string} fn — 'listControls' | 'setControlValue'.
|
|
94
|
+
* @param {Array} args — verb-specific positional args (name first, always).
|
|
95
|
+
* @returns {*} plain, structured-clone-serializable data.
|
|
96
|
+
* @throws {Error} FAIL-LOUD — unknown instance name (registry.getControls's
|
|
97
|
+
* own guard), unknown control id, an unrecognized verb, or (setControlValue)
|
|
98
|
+
* a descriptor with neither `onChange` nor `bind` reachable (structurally
|
|
99
|
+
* should not happen post-normalize — lib/shim/controls requires at least
|
|
100
|
+
* one — but dispatch never silently no-ops if it somehow does).
|
|
101
|
+
*/
|
|
102
|
+
function _dispatchControlsCall(registry, fn, args) {
|
|
103
|
+
args = args || [];
|
|
104
|
+
if (fn === 'listControls') {
|
|
105
|
+
var name = args[0];
|
|
106
|
+
var descs = registry.getControls(name); // FAIL-LOUD on unknown name
|
|
107
|
+
var bindCtx0 = registry._controlBindCtx;
|
|
108
|
+
return descs.map(function (d) {
|
|
109
|
+
// Bind-based descriptors refresh from LIVE state before serializing —
|
|
110
|
+
// this is what makes listControls a genuine PULL/reconciliation source
|
|
111
|
+
// (frqtl-control-sync.js, WS gym-controls-as-sugar FULL CONVERGENCE
|
|
112
|
+
// Stage ii) rather than a stale cache of the last WRITE. Per the
|
|
113
|
+
// declared non-uniform get() contract (controlBind.js): Frame binds
|
|
114
|
+
// read live; LargeObject/backdrop binds read the last TickData
|
|
115
|
+
// snapshot (only if exposeRegistry's opts.readSnapshot was supplied);
|
|
116
|
+
// render binds and onChange-only descriptors have NO read — a
|
|
117
|
+
// `get()` of `undefined` is a DEFINED SKIP, so `d.value` (the last
|
|
118
|
+
// known value) is kept, never overwritten with a fabricated read.
|
|
119
|
+
// A get()-side wiring gap (e.g. ctx.universe present for writes but
|
|
120
|
+
// missing the read accessor) degrades this ONE descriptor's refresh
|
|
121
|
+
// to a no-op (keep the cached value) rather than failing the WHOLE
|
|
122
|
+
// listing — logged, never silently swallowed. Structural bind
|
|
123
|
+
// MISDECLARATION (bad bind.object/prop shape) still throws through
|
|
124
|
+
// setControlValue's write path; this is a READ-side resilience
|
|
125
|
+
// allowance only.
|
|
126
|
+
if (d.bind && bindCtx0) {
|
|
127
|
+
var ControlBind0 = _ControlBindNode;
|
|
128
|
+
try {
|
|
129
|
+
var live = ControlBind0.resolveBind(d.bind, bindCtx0, d.channel).get();
|
|
130
|
+
if (live !== undefined) { d.value = live; }
|
|
131
|
+
} catch (getErr) {
|
|
132
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
133
|
+
console.warn('[frqtl-controls-panel] listControls: live refresh failed for "' +
|
|
134
|
+
d.id + '" (kept cached value):', getErr);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// The wire row carries the DECLARED presentation vocabulary too
|
|
139
|
+
// (kind/widget/place/action) — all plain, structured-clone-serializable
|
|
140
|
+
// data. Without it a host could not know a descriptor is a toggle, a
|
|
141
|
+
// button, or canvas-PLACED, and would have to re-derive it (the
|
|
142
|
+
// second-authority bug). Absent keys stay absent (a scalar slider row
|
|
143
|
+
// is byte-identical to before).
|
|
144
|
+
var row = {
|
|
145
|
+
id: d.id, label: d.label, min: d.min, max: d.max, step: d.step,
|
|
146
|
+
value: d.value,
|
|
147
|
+
valueText: (typeof d.fmt === 'function') ? d.fmt(d.value) : String(d.value)
|
|
148
|
+
};
|
|
149
|
+
if (d.kind !== undefined) { row.kind = d.kind; }
|
|
150
|
+
if (d.widget !== undefined) { row.widget = d.widget; }
|
|
151
|
+
if (d.action !== undefined) { row.action = d.action; }
|
|
152
|
+
if (d.values !== undefined) { row.values = d.values; }
|
|
153
|
+
if (d.place !== undefined) { row.place = d.place; }
|
|
154
|
+
return row;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (fn === 'runControlAction') {
|
|
158
|
+
// The ACTION verb (presentation pass Item 2): resolve the descriptor by id,
|
|
159
|
+
// then run it through the ONE objects-side executor (controlAction.js), which
|
|
160
|
+
// maps the blessed action NAME to a live runner method. The dispatch point
|
|
161
|
+
// stays single — every host (page button, panel, canvas overlay) arrives here.
|
|
162
|
+
var nameA = args[0], idA = args[1];
|
|
163
|
+
var descsA = registry.getControls(nameA); // FAIL-LOUD on unknown name
|
|
164
|
+
var hit = null;
|
|
165
|
+
for (var a = 0; a < descsA.length; a++) { if (descsA[a].id === idA) { hit = descsA[a]; break; } }
|
|
166
|
+
if (!hit) {
|
|
167
|
+
throw new Error('[frqtl-controls-panel] runControlAction: unknown control id "' + idA +
|
|
168
|
+
'" in "' + nameA + '" (registered: [' +
|
|
169
|
+
descsA.map(function (c) { return c.id; }).join(', ') + ']). (FAIL-LOUD.)');
|
|
170
|
+
}
|
|
171
|
+
if (hit.kind !== 'action') {
|
|
172
|
+
throw new Error('[frqtl-controls-panel] runControlAction("' + idA + '"): descriptor kind is "' +
|
|
173
|
+
hit.kind + '", not "action". (FAIL-LOUD.)');
|
|
174
|
+
}
|
|
175
|
+
var ctxA = registry._controlBindCtx || {};
|
|
176
|
+
_ControlActionNode.resolveAction(hit.action, { runner: ctxA.runner }).run();
|
|
177
|
+
return { action: hit.action };
|
|
178
|
+
}
|
|
179
|
+
if (fn === 'setControlValue') {
|
|
180
|
+
var name2 = args[0], id = args[1], value = args[2];
|
|
181
|
+
var descs2 = registry.getControls(name2); // FAIL-LOUD on unknown name
|
|
182
|
+
var match = null;
|
|
183
|
+
for (var i = 0; i < descs2.length; i++) { if (descs2[i].id === id) { match = descs2[i]; break; } }
|
|
184
|
+
if (!match) {
|
|
185
|
+
throw new Error('[frqtl-controls-panel] setControlValue: unknown control id "' + id +
|
|
186
|
+
'" in "' + name2 + '" (registered: [' +
|
|
187
|
+
descs2.map(function (c) { return c.id; }).join(', ') + ']). (FAIL-LOUD.)');
|
|
188
|
+
}
|
|
189
|
+
match.value = value;
|
|
190
|
+
// Write-authority dispatch (§5 converged ruling — "at least one of
|
|
191
|
+
// {onChange, bind}", never neither): onChange wins if present (it may
|
|
192
|
+
// coexist with a READ-only bind); otherwise bind.set() executes via
|
|
193
|
+
// controlBind.js, routed through the universe/runner exposeRegistry
|
|
194
|
+
// threaded in; a descriptor reaching here with NEITHER is a dead
|
|
195
|
+
// control that should have been caught at normalize time — dispatch
|
|
196
|
+
// THROWS rather than silently no-op (the bug this build fixes).
|
|
197
|
+
if (typeof match.onChange === 'function') {
|
|
198
|
+
match.onChange(value);
|
|
199
|
+
} else if (match.bind) {
|
|
200
|
+
var bindCtx = registry._controlBindCtx || {};
|
|
201
|
+
var ControlBind = _ControlBindNode;
|
|
202
|
+
var bound = ControlBind.resolveBind(match.bind, bindCtx, match.channel);
|
|
203
|
+
bound.set(value);
|
|
204
|
+
} else {
|
|
205
|
+
throw new Error('[frqtl-controls-panel] setControlValue("' + id + '" in "' + name2 +
|
|
206
|
+
'"): descriptor has neither "onChange" nor "bind" — this should be structurally ' +
|
|
207
|
+
'impossible post-normalize (lib/shim/controls requires at least one). (FAIL-LOUD — ' +
|
|
208
|
+
'never a silent no-op.)');
|
|
209
|
+
}
|
|
210
|
+
return { value: value, valueText: (typeof match.fmt === 'function') ? match.fmt(value) : String(value) };
|
|
211
|
+
}
|
|
212
|
+
throw new Error('[frqtl-controls-panel] unrecognized verb "' + String(fn) +
|
|
213
|
+
'" (expected "listControls", "setControlValue" or "runControlAction"). (FAIL-LOUD.)');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* exposeRegistry(registry, opts) — REALM side (call on the experiment page).
|
|
218
|
+
*
|
|
219
|
+
* @param {object} registry — a makeControlRegistry() instance with its
|
|
220
|
+
* controls already registered (registerControls already called).
|
|
221
|
+
* @param {object} [opts]
|
|
222
|
+
* @param {boolean} [opts.crossFrame=true] — install the RPC responder so a
|
|
223
|
+
* parent frame's panel (mode:'iframe') can drive this page. Set false to
|
|
224
|
+
* expose in-app access only (e.g. a page with no host frame expected).
|
|
225
|
+
* @param {object} [opts.universe] — the live EGPTUniverse (WS gym-controls-as-sugar
|
|
226
|
+
* FULL CONVERGENCE build, signature change): threaded into `controlBind.js`'s
|
|
227
|
+
* `resolveBind` ctx so a `bind`-only descriptor's `setControlValue` can route
|
|
228
|
+
* through `setFrameProperty`/`updateObjectProperty`. Required only if the
|
|
229
|
+
* page registers any bind-only (no `onChange`) physics-channel control.
|
|
230
|
+
* @param {object} [opts.runner] — the FrqtlApp. Required only if the
|
|
231
|
+
* page registers any bind-only render-channel control (`runner.setRenderOption`).
|
|
232
|
+
* @param {function} [opts.readSnapshot] — `(handle, kind, prop) => *|undefined`,
|
|
233
|
+
* the LargeObject/backdrop bind READ path (last `TickData.objects[handle]`
|
|
234
|
+
* snapshot) — see `controlBind.js`'s own docstring. Omitting it makes those
|
|
235
|
+
* binds' `get()` a defined skip (`undefined`), never a fabricated read.
|
|
236
|
+
* @throws {Error} FAIL-LOUD if `registry` is not a registry instance.
|
|
237
|
+
*/
|
|
238
|
+
/** The registry this page last exposed (exposeRegistry), or null — the page door's `controls`. */
|
|
239
|
+
var _exposedRegistry = null;
|
|
240
|
+
function exposedRegistry() { return _exposedRegistry; }
|
|
241
|
+
|
|
242
|
+
function exposeRegistry(registry, opts) {
|
|
243
|
+
opts = opts || {};
|
|
244
|
+
if (!registry || typeof registry.getControls !== 'function') {
|
|
245
|
+
throw new Error('[frqtl-controls-panel] exposeRegistry requires a makeControlRegistry() ' +
|
|
246
|
+
'instance (missing getControls). (FAIL-LOUD.)');
|
|
247
|
+
}
|
|
248
|
+
// The page's exposed registry — what a host frame discovers through the page door
|
|
249
|
+
// (FrqtlPage.controls). A same-window panel (mode:'in-app') is handed this SAME instance.
|
|
250
|
+
_exposedRegistry = registry;
|
|
251
|
+
|
|
252
|
+
// Bind-execution context (WS gym-controls-as-sugar FULL CONVERGENCE build):
|
|
253
|
+
// attached to the registry INSTANCE (not a fresh object, not a second
|
|
254
|
+
// parameter threaded through mount()/_buildInAppHandle) so BOTH dispatch
|
|
255
|
+
// paths — the in-app direct call (which is handed this SAME registry
|
|
256
|
+
// instance by mount({mode:'in-app'})) and the cross-frame RPC responder
|
|
257
|
+
// below — resolve the identical universe/runner/readSnapshot via
|
|
258
|
+
// `registry._controlBindCtx` inside `_dispatchControlsCall`. `registry`
|
|
259
|
+
// itself doubles as `controlBind.js`'s `ctx.registry` (the SAME
|
|
260
|
+
// makeControlRegistry() instance unifies the objects catalog
|
|
261
|
+
// getObject/getObjectKind with the controls catalog — no second registry
|
|
262
|
+
// object needed).
|
|
263
|
+
registry._controlBindCtx = {
|
|
264
|
+
registry: registry,
|
|
265
|
+
universe: opts.universe,
|
|
266
|
+
runner: opts.runner,
|
|
267
|
+
readSnapshot: opts.readSnapshot
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (opts.crossFrame === false) { return; }
|
|
271
|
+
// The cross-frame responder is a PAGE's (a realm with no document — node, a worker — has no host
|
|
272
|
+
// frame to answer); the exposed registry and its bind context above hold in any realm.
|
|
273
|
+
if (typeof document === 'undefined') { return; }
|
|
274
|
+
var view = document.defaultView;
|
|
275
|
+
if (!view || !view.parent || view.parent === view) { return; } // not framed — nothing to respond to
|
|
276
|
+
|
|
277
|
+
var ShimRpc = _ShimRpcNode;
|
|
278
|
+
var channel = ShimRpc.makeWindowPostMessageChannel(view.parent, { selfWindow: view });
|
|
279
|
+
ShimRpc.installRpcResponder(channel, function (fn, args) {
|
|
280
|
+
return _dispatchControlsCall(registry, fn, args);
|
|
281
|
+
}, { tag: RPC_TAG });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Build the in-app RPC handle: `call()` dispatches directly, no round-trip. */
|
|
285
|
+
function _buildInAppHandle(registry) {
|
|
286
|
+
return _ShimRpcNode.makeInAppRpc(function (fn, args) { return _dispatchControlsCall(registry, fn, args); });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Build the iframe RPC handle: `call()` marshals over postMessage.
|
|
291
|
+
* @param {HTMLIFrameElement} frame
|
|
292
|
+
* @param {number} [callTimeoutMs] — override for tests only (production
|
|
293
|
+
* callers use lib/shim/rpc's own 3s default; see `mount`'s `opts.callTimeoutMs`).
|
|
294
|
+
*/
|
|
295
|
+
function _buildIframeHandle(frame, callTimeoutMs) {
|
|
296
|
+
if (!frame || !frame.contentWindow) {
|
|
297
|
+
throw new Error('[frqtl-controls-panel] mount({mode:"iframe"}) requires opts.frame — an ' +
|
|
298
|
+
'<iframe> element with a live contentWindow. (FAIL-LOUD — missing transport.)');
|
|
299
|
+
}
|
|
300
|
+
var ShimRpc = _ShimRpcNode;
|
|
301
|
+
var channel = ShimRpc.makeWindowPostMessageChannel(frame.contentWindow);
|
|
302
|
+
var rpcOpts = { tag: RPC_TAG };
|
|
303
|
+
if (typeof callTimeoutMs === 'number' && callTimeoutMs > 0) { rpcOpts.callTimeoutMs = callTimeoutMs; }
|
|
304
|
+
return ShimRpc.makeChannelRpc(channel, rpcOpts);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* mount(hostEl, opts) — PANEL side.
|
|
309
|
+
*
|
|
310
|
+
* @param {HTMLElement} hostEl — the div to render controls into.
|
|
311
|
+
* @param {object} opts
|
|
312
|
+
* @param {'in-app'|'iframe'} opts.mode
|
|
313
|
+
* @param {string} opts.name — the registry instance name `exposeRegistry`'s
|
|
314
|
+
* registry registered its controls under (e.g. 'collision-matrix').
|
|
315
|
+
* @param {object} [opts.registry] — REQUIRED for `mode:'in-app'`: the SAME
|
|
316
|
+
* registry instance the target page's `exposeRegistry(registry)` call used
|
|
317
|
+
* (same realm — `FrqtlControlsPanel.exposedRegistry()`, or the page door's
|
|
318
|
+
* `FrqtlPage.controls` from a parent frame, if the caller does not already hold a reference).
|
|
319
|
+
* @param {HTMLIFrameElement} [opts.frame] — REQUIRED for `mode:'iframe'`.
|
|
320
|
+
* @param {number} [opts.callTimeoutMs] — `mode:'iframe'` only: override the
|
|
321
|
+
* default 3s per-call timeout (tests only; production callers should not
|
|
322
|
+
* need this).
|
|
323
|
+
* @returns {{dispose: () => void, ready: Promise<void>}} `ready` resolves once
|
|
324
|
+
* the initial control list has rendered, or rejects if the target was not
|
|
325
|
+
* yet reachable (e.g. an iframe target still booting) — a caller MAY
|
|
326
|
+
* `dispose()` and retry `mount()` on rejection (Layer A has no built-in
|
|
327
|
+
* readiness handshake/retry).
|
|
328
|
+
* @throws {Error} FAIL-LOUD on a missing host element, missing `opts.name`,
|
|
329
|
+
* an unrecognized `opts.mode`, or a missing transport target
|
|
330
|
+
* (`opts.registry`/`opts.frame` per mode).
|
|
331
|
+
*/
|
|
332
|
+
function mount(hostEl, opts) {
|
|
333
|
+
opts = opts || {};
|
|
334
|
+
if (!hostEl) {
|
|
335
|
+
throw new Error('[frqtl-controls-panel] mount requires a host element. (FAIL-LOUD.)');
|
|
336
|
+
}
|
|
337
|
+
if (!opts.name) {
|
|
338
|
+
throw new Error('[frqtl-controls-panel] mount requires opts.name (the registry instance ' +
|
|
339
|
+
'name registered via exposeRegistry). (FAIL-LOUD.)');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
var handle;
|
|
343
|
+
if (opts.mode === 'in-app') {
|
|
344
|
+
if (!opts.registry) {
|
|
345
|
+
throw new Error('[frqtl-controls-panel] mount({mode:"in-app"}) requires opts.registry ' +
|
|
346
|
+
'(the SAME registry instance exposeRegistry() was called with). (FAIL-LOUD.)');
|
|
347
|
+
}
|
|
348
|
+
handle = _buildInAppHandle(opts.registry);
|
|
349
|
+
} else if (opts.mode === 'iframe') {
|
|
350
|
+
handle = _buildIframeHandle(opts.frame, opts.callTimeoutMs);
|
|
351
|
+
} else {
|
|
352
|
+
throw new Error('[frqtl-controls-panel] mount: opts.mode must be "in-app" or "iframe"; got ' +
|
|
353
|
+
JSON.stringify(opts.mode) + '. (FAIL-LOUD.)');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
var name = opts.name;
|
|
357
|
+
hostEl.classList.add('frqtl-controls-panel');
|
|
358
|
+
|
|
359
|
+
// Rendering is delegated ENTIRELY to frqtl-control-slider.js (Bite 3
|
|
360
|
+
// Stage 2 Layer B: "panel = collection host + transport plumbing;
|
|
361
|
+
// control lib = the widget") — this closure supplies ONLY the
|
|
362
|
+
// transport-aware onInput, reusing the SAME `handle.call(...)` this
|
|
363
|
+
// file already builds for either placement mode.
|
|
364
|
+
function renderRows(descs) {
|
|
365
|
+
hostEl.innerHTML = '';
|
|
366
|
+
var Widgets = _ControlWidgetsNode;
|
|
367
|
+
descs.forEach(function (d) {
|
|
368
|
+
Widgets.mountWidget(hostEl, {
|
|
369
|
+
initial: d,
|
|
370
|
+
onInput: function (v) { return handle.call('setControlValue', [name, d.id, v]); },
|
|
371
|
+
onAction: function () { return handle.call('runControlAction', [name, d.id]); }
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// `ready` lets a host (e.g. an iframe-mode caller mounting before the
|
|
377
|
+
// target page has finished booting its engine/renderer and reached its
|
|
378
|
+
// own exposeRegistry() call — there is no built-in readiness handshake
|
|
379
|
+
// in Layer A) detect a failed initial fetch and retry by disposing this
|
|
380
|
+
// handle and calling mount() again. Rejecting here does NOT throw
|
|
381
|
+
// synchronously — mount() itself never fails merely because the target
|
|
382
|
+
// isn't ready yet; only a genuinely malformed call (FAIL-LOUD above)
|
|
383
|
+
// throws synchronously.
|
|
384
|
+
var ready = handle.call('listControls', [name]).then(function (descs) {
|
|
385
|
+
renderRows(descs);
|
|
386
|
+
}).catch(function (e) {
|
|
387
|
+
hostEl.textContent = '[frqtl-controls-panel] failed to load "' + name + '": ' +
|
|
388
|
+
(e && e.message ? e.message : String(e));
|
|
389
|
+
if (typeof console !== 'undefined' && console.error) { console.error(e); }
|
|
390
|
+
throw e;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
ready: ready,
|
|
395
|
+
dispose: function () {
|
|
396
|
+
handle.dispose();
|
|
397
|
+
hostEl.innerHTML = '';
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
// `dispatch` publicly exposes the SAME single dispatch point the in-app path
|
|
405
|
+
// and the RPC responder already share (WS gym-controls-as-sugar FULL
|
|
406
|
+
// CONVERGENCE build) — its THIRD consumer is `frqtl-control-sync.js`'s in-app
|
|
407
|
+
// pull path (`registry` mode), which reuses `dispatch(registry, 'listControls',
|
|
408
|
+
// [name])` rather than re-deriving a parallel listing implementation, and its
|
|
409
|
+
// FOURTH is `frqtl-control-overlay.js` (the canvas overlay, presentation pass
|
|
410
|
+
// Item 2) — every overlay write and every overlay action verb routes through
|
|
411
|
+
// THIS function, so the overlay adds no parallel write authority.
|
|
412
|
+
var FrqtlControlsPanel = { exposeRegistry: exposeRegistry, exposedRegistry: exposedRegistry, mount: mount, dispatch: _dispatchControlsCall };
|
|
413
|
+
|
|
414
|
+
export { FrqtlControlsPanel };
|
|
415
|
+
|