@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,627 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/controls — The control-descriptor schema contract.
|
|
5
|
+
*
|
|
6
|
+
* Single authority for "what shape is a live control knob" (WS gym-controls-as-sugar
|
|
7
|
+
* Bite 1 ratified decisions).
|
|
8
|
+
* Peer of `lib/shim/frame-data` (the data-bubble schema) and `lib/shim/expr/ast-schema`
|
|
9
|
+
* (the op-program AST schema) — same file/export layout convention (a closed vocabulary
|
|
10
|
+
* + a throw-or-apply `normalize`, named ES-module exports).
|
|
11
|
+
*
|
|
12
|
+
* ── Why this exists ──────────────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* Every FRQTL experiment that wants a live, no-reload knob has independently invented
|
|
15
|
+
* its OWN capability-object shape, each proven correct in the field but never codified
|
|
16
|
+
* in one place:
|
|
17
|
+
* - `runner.experimentControls` — the SCALAR slider/toggle shape, first standardized
|
|
18
|
+
* by `SphereEmitter.controls` (`lib/frqtl/parts/SphereEmitter.js`), then
|
|
19
|
+
* reused verbatim by `collision-matrix-3d` and `g3-lo-collide`.
|
|
20
|
+
* - `runner.cameraControl` — the CAMERA orbit handle, from
|
|
21
|
+
* `three-provider.js getCameraControl()`.
|
|
22
|
+
* - `runner.loopControl` — the LOOP periodic-reset control, from
|
|
23
|
+
* `fx-experiment.js _buildLoopControl()`.
|
|
24
|
+
*
|
|
25
|
+
* This module does NOT invent new shapes — it CODIFIES these three already-shipped,
|
|
26
|
+
* already-battle-tested shapes as a closed kind union, so a NEW sugar object can
|
|
27
|
+
* `normalizeControlDescriptor({kind:'scalar', ...})` and get the SAME throw-or-apply
|
|
28
|
+
* guarantee every future host (a control panel, the GYM, Studio) can rely on, instead
|
|
29
|
+
* of every consumer re-deriving "does this look like a camera control?" by hand
|
|
30
|
+
* (the schema-mirror-drift anti-pattern root `CLAUDE.md` calls out).
|
|
31
|
+
*
|
|
32
|
+
* ── Scope (Bite 1 — contract only) ───────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* This module ships the SCHEMA + normalize. It does NOT:
|
|
35
|
+
* - render any UI panel (that is Bite 3's `frqtl-control-*.js` family),
|
|
36
|
+
* - touch the GYM/CompareView.jsx (Bite 2 — still duck-types these capabilities
|
|
37
|
+
* ad hoc; migrating it to `normalizeControlDescriptor` is a later bite's job),
|
|
38
|
+
* - migrate any existing experiment page's `CONTROLS = [...]` literal onto this
|
|
39
|
+
* module (byte-identical arrays remain valid input to `normalizeControlDescriptor`
|
|
40
|
+
* — normalizing is OPT-IN for a new consumer, not a retrofit requirement).
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* import { KINDS, CHANNELS, normalizeControlDescriptor } from 'lib/shim/controls/index.js';
|
|
44
|
+
* const bag = normalizeControlDescriptor({
|
|
45
|
+
* kind: 'scalar', id: 'chamberFlow', label: 'flow',
|
|
46
|
+
* min: 0, max: 1, step: 1, value: 1,
|
|
47
|
+
* fmt: (v) => (v ? 'on' : 'off'),
|
|
48
|
+
* onChange: (v) => self.setActive(!!v)
|
|
49
|
+
* });
|
|
50
|
+
* // bag.channel === 'physics' (the scalar default — see CHANNELS below)
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
// ── Kind union (closed) ─────────────────────────────────────────────────────────
|
|
56
|
+
//
|
|
57
|
+
// Conformance iterates this list — adding a kind here MUST be accompanied by a
|
|
58
|
+
// REQUIRED_KEYS_BY_KIND entry + a validator branch in normalizeControlDescriptor
|
|
59
|
+
// + a conformance scenario (mirrors ast-schema.js NODE_KINDS discipline: "closed at N,
|
|
60
|
+
// do not add without updating the iteration surface").
|
|
61
|
+
//
|
|
62
|
+
// scalar — a single numeric slider/toggle knob. THE PROVEN SphereEmitter.controls
|
|
63
|
+
// shape: {id,label,min,max,step,value,fmt,onChange}. A toggle is a scalar
|
|
64
|
+
// with min:0,max:1,step:1 — NOT a separate kind (closed-union discipline).
|
|
65
|
+
// camera — an orbit-camera handle. THE PROVEN three-provider.js getCameraControl()
|
|
66
|
+
// shape: {presets,setOrbit,getOrbit,setPreset}.
|
|
67
|
+
// loop — a periodic runner.setup() re-run control. THE PROVEN fx-experiment.js
|
|
68
|
+
// _buildLoopControl() shape: {enabled,ticks,setEnabled,setTicks}.
|
|
69
|
+
//
|
|
70
|
+
// The three kinds below are NEW (WS Studio Controls Realignment S1, design §2.2 /
|
|
71
|
+
// gap-census #1,#4,#6,#10 — architect-ratified: "only these three are genuinely new
|
|
72
|
+
// value-shapes"):
|
|
73
|
+
//
|
|
74
|
+
// enum — select-one from a closed candidate set. Covers Studio's
|
|
75
|
+
// collision_type/gateType/reposition Inspector fields (census #6).
|
|
76
|
+
// Shape: {id,label,values,value,fmt,onChange,bind}. `value` MUST be a
|
|
77
|
+
// member of `values` (both single-select semantics enforced here).
|
|
78
|
+
// flags — multi-select from a closed candidate set. Covers Studio's `actions`
|
|
79
|
+
// (schema type 'enum-set') Inspector field. Shape:
|
|
80
|
+
// {id,label,values,value,fmt,onChange,bind}. `value` is an ARRAY, each
|
|
81
|
+
// element MUST be a member of `values`.
|
|
82
|
+
// vector — an ordered numeric tuple, written WHOLE (never a torn per-component
|
|
83
|
+
// write). THE placement/drag answer (census #4/#5, Transport Fit §2):
|
|
84
|
+
// covers x/y(/z), the 2D rect [l,t,w,h], the 3D 6-tuple
|
|
85
|
+
// [l,t,z,w,h,d] (LargeObject.rect already documents z/d — see
|
|
86
|
+
// reference_lo_z_depth_wire_contract), and rgb [r,g,b]. Length is
|
|
87
|
+
// FREE (not fixed at construction) — a 2-tuple, 4-tuple, and 6-tuple
|
|
88
|
+
// are all the same kind, just different `value.length`. Shape:
|
|
89
|
+
// {id,label,value,min,max,step,fmt,onChange,bind} — `min`/`max`/`step`
|
|
90
|
+
// are each EITHER one shared number (applies to every component) OR
|
|
91
|
+
// an array the SAME LENGTH as `value` (a per-component bound, e.g. a
|
|
92
|
+
// rect's `w`/`h` needing a different range than `left`/`top`).
|
|
93
|
+
//
|
|
94
|
+
// The two kinds below are NEW (presentation pass Item 2, CEO-ratified E-P2a) —
|
|
95
|
+
// the two value shapes no existing kind holds:
|
|
96
|
+
//
|
|
97
|
+
// text — a STRING-valued control: {id,label,value,fmt,onChange} (+ optional
|
|
98
|
+
// `bind`). Every other kind's `value` is a number, a number tuple, or
|
|
99
|
+
// a member/subset of a closed candidate set; a free string is none of
|
|
100
|
+
// those.
|
|
101
|
+
// action — a MOMENTARY verb: {id,label,action} where `action` is a member of
|
|
102
|
+
// the closed ACTIONS vocabulary below. Stateless (no `value`, no
|
|
103
|
+
// read-back) and serializable by NAME, so a declared array of them
|
|
104
|
+
// round-trips through codegen with no new machinery. Actions are
|
|
105
|
+
// deliberately NOT expressed as a `bind` — a bind is a property
|
|
106
|
+
// read/write pair, and `get()` of "restart" is meaningless.
|
|
107
|
+
//
|
|
108
|
+
// Deliberately NOT separate kinds (architect ruling, closed-union discipline —
|
|
109
|
+
// do not propose adding these):
|
|
110
|
+
// - `bool`/`toggle` — a toggle IS a `scalar` with min:0,max:1,step:1 (matches
|
|
111
|
+
// the pre-existing scalar-toggle convention above; no shape distinction).
|
|
112
|
+
// - `color`/`rgb` — an RGB color IS a `vector` of length 3 (`[r,g,b]`) with
|
|
113
|
+
// a color-picker WIDGET override via the kind-orthogonal `resolveWidget`
|
|
114
|
+
// seam (a rendering/widget-selection choice, not a value-shape difference —
|
|
115
|
+
// `normalizeControlDescriptor` never picks widgets, only validates shape).
|
|
116
|
+
var KINDS = Object.freeze(['scalar', 'camera', 'loop', 'enum', 'flags', 'vector',
|
|
117
|
+
'text', 'action']);
|
|
118
|
+
|
|
119
|
+
// ── Game-layer ACTION vocabulary (closed) ───────────────────────────────────────
|
|
120
|
+
//
|
|
121
|
+
// The names an `action` descriptor may carry. The shim owns the NAME LIST (the
|
|
122
|
+
// data shape); EXECUTION is objects-side — `lib/frqtl/objects/objects/controlAction.js`
|
|
123
|
+
// maps each name to a live FrqtlApp method. This is the SAME arrow split
|
|
124
|
+
// the `bind` contract already uses (shim owns the shape, objects owns execution),
|
|
125
|
+
// and it is why no page ever re-implements pause/restart plumbing.
|
|
126
|
+
//
|
|
127
|
+
// pause — stop the run loop resume — start it again
|
|
128
|
+
// toggle — flip play/pause restart — replay the retained setup
|
|
129
|
+
// factory in-place (tick 0,
|
|
130
|
+
// no page reload)
|
|
131
|
+
var ACTIONS = Object.freeze(['pause', 'resume', 'toggle', 'restart']);
|
|
132
|
+
|
|
133
|
+
// ── Placement (optional, any kind) ─────────────────────────────────────────────
|
|
134
|
+
//
|
|
135
|
+
// `place: {x, y, anchor}` marks a descriptor as drawable OVER THE CANVAS by a
|
|
136
|
+
// host overlay (lib/frqtl/parts/FrqtlControlOverlay.js). x/y are
|
|
137
|
+
// FRACTIONS of the canvas (0..1) so a placement survives a resize; `anchor` picks
|
|
138
|
+
// which corner they are measured from. A descriptor WITHOUT `place` behaves
|
|
139
|
+
// exactly as before (panel hosts only) — this module validates the SHAPE only and
|
|
140
|
+
// never positions anything itself.
|
|
141
|
+
var PLACE_KEYS = Object.freeze(['x', 'y', 'anchor']);
|
|
142
|
+
var PLACE_ANCHORS = Object.freeze(['top-left', 'top-right', 'bottom-left', 'bottom-right']);
|
|
143
|
+
var DEFAULT_PLACE_ANCHOR = 'top-left';
|
|
144
|
+
|
|
145
|
+
// ── Widget seam (optional, kind-ORTHOGONAL) ────────────────────────────────────
|
|
146
|
+
//
|
|
147
|
+
// The schema's standing ruling above ("a toggle IS a scalar 0/1"; "a color IS a
|
|
148
|
+
// vector-3") requires a place to say WHICH widget renders a given descriptor
|
|
149
|
+
// without inventing a kind for it. `widget` is that seam: a closed name list,
|
|
150
|
+
// validated here, resolved to an actual renderer by the ONE widget table
|
|
151
|
+
// (lib/frqtl/objects/objects/frqtl-control-widgets.js). `toggle`/`checkbox` are
|
|
152
|
+
// widgets over a scalar 0/1; `button` is the action kind's widget.
|
|
153
|
+
var WIDGETS = Object.freeze(['slider', 'toggle', 'checkbox', 'text', 'button']);
|
|
154
|
+
|
|
155
|
+
// ── Channel discriminator (closed, ratified §2) ─────────────────────────────────
|
|
156
|
+
//
|
|
157
|
+
// Separates render-knob writes (a control whose onChange ultimately calls
|
|
158
|
+
// runner.setRenderOption(key,value) — a VISUAL/render-intent write) from
|
|
159
|
+
// physics-knob writes (a control whose onChange calls a capability/property
|
|
160
|
+
// accessor on a sugar object or Frame/LargeObject — a PHYSICS-parametric write).
|
|
161
|
+
// This is metadata for future host tooling (e.g. a control panel choosing an icon
|
|
162
|
+
// or a batching strategy) — normalizeControlDescriptor never calls onChange itself
|
|
163
|
+
// and never inspects its closure body; `channel` is a DECLARED tag the descriptor's
|
|
164
|
+
// AUTHOR supplies, not something inferred.
|
|
165
|
+
//
|
|
166
|
+
// ── Channel-semantics clarification (design §2.2, corrects a natural misreading) ──
|
|
167
|
+
// `channel` discriminates the WRITE PRIMITIVE, NOT `engineVisible`:
|
|
168
|
+
// - 'physics' → the write goes through an OBJECT-PROP accessor
|
|
169
|
+
// (`runner.updateObjectProperty` / `setFrameProperty` / `setUniverseProperty`).
|
|
170
|
+
// This includes every per-object prop, EVEN an `engineVisible:false` one
|
|
171
|
+
// (color, alpha, collision_type, actions, name) — those are still
|
|
172
|
+
// 'physics'-channel OBJECT BINDS, they just happen to be render-only at the
|
|
173
|
+
// engine-ABI level (mutate the JS wrapper, never cross into the engine).
|
|
174
|
+
// - 'render' → the write goes through the PAGE-LEVEL `runner.setRenderOption`
|
|
175
|
+
// (godrays, glow, colorScheme, shape) — a render-INTENT knob with no
|
|
176
|
+
// per-object identity at all.
|
|
177
|
+
// A schema field's `engineVisible` flag is an ORTHOGONAL fact about whether the
|
|
178
|
+
// engine reads that field back — it does NOT determine `channel`. (An earlier,
|
|
179
|
+
// REJECTED proposal to map `engineVisible → channel` directly is wrong: every
|
|
180
|
+
// per-object field, `engineVisible` or not, is a 'physics'-channel object bind.)
|
|
181
|
+
//
|
|
182
|
+
// channel applies most meaningfully to 'scalar'/'enum'/'flags'/'vector' (the
|
|
183
|
+
// kinds whose onChange is a bare function, otherwise indistinguishable from
|
|
184
|
+
// either use). It is OPTIONAL, but VALIDATED IF PRESENT, on
|
|
185
|
+
// 'camera'/'loop' — their own kind-specific methods
|
|
186
|
+
// (setOrbit/setEnabled) are already self-describing capability
|
|
187
|
+
// surfaces, so this schema does not force a channel value onto them.
|
|
188
|
+
var CHANNELS = Object.freeze(['physics', 'render']);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* DEFAULT_CHANNEL — applied to a descriptor of a DEFAULT_CHANNEL_KINDS member
|
|
192
|
+
* that omits `channel`.
|
|
193
|
+
* 'physics' matches EVERY existing scalar consumer byte-for-byte (SphereEmitter,
|
|
194
|
+
* collision-matrix-3d and g3-lo-collide both drive capability/property
|
|
195
|
+
* accessors, never setRenderOption, and none of them declare `channel` today) —
|
|
196
|
+
* this default keeps every existing `CONTROLS = [...]` array a valid, unchanged
|
|
197
|
+
* normalizeControlDescriptor input with no source-file migration required.
|
|
198
|
+
*/
|
|
199
|
+
var DEFAULT_CHANNEL = 'physics';
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* DEFAULT_CHANNEL_KINDS — the kinds whose omitted `channel` resolves to
|
|
203
|
+
* DEFAULT_CHANNEL (S1: extended from 'scalar'-only to every bare-onChange kind
|
|
204
|
+
* — 'enum'/'flags'/'vector' share the exact same ambiguity `scalar` has: their
|
|
205
|
+
* onChange is a plain function, otherwise indistinguishable between a physics
|
|
206
|
+
* write and a render write). 'camera'/'loop' are NOT members — their
|
|
207
|
+
* own kind-specific methods are already self-describing capability surfaces.
|
|
208
|
+
*/
|
|
209
|
+
var DEFAULT_CHANNEL_KINDS = Object.freeze(['scalar', 'enum', 'flags', 'vector', 'text']);
|
|
210
|
+
|
|
211
|
+
// ── Per-kind required-key tables ─────────────────────────────────────────────────
|
|
212
|
+
//
|
|
213
|
+
// Conformance iterates REQUIRED_KEYS_BY_KIND[kind] for each kind in KINDS — never a
|
|
214
|
+
// hand-copied per-kind list in the test file (the schema-mirror-drift discipline).
|
|
215
|
+
|
|
216
|
+
/** SCALAR — {id,label,min,max,step,value,fmt,onChange}. `id` is validated separately
|
|
217
|
+
* (every kind requires it, for registry addressing) — SCALAR_KEYS lists the
|
|
218
|
+
* kind-SPECIFIC fields beyond the common `{kind,id}` envelope. */
|
|
219
|
+
var SCALAR_KEYS = Object.freeze(['label', 'min', 'max', 'step', 'value', 'fmt', 'onChange']);
|
|
220
|
+
|
|
221
|
+
/** CAMERA — {presets,setOrbit,getOrbit,setPreset}, the exact getCameraControl() shape. */
|
|
222
|
+
var CAMERA_KEYS = Object.freeze(['presets', 'setOrbit', 'getOrbit', 'setPreset']);
|
|
223
|
+
|
|
224
|
+
/** LOOP — {enabled,ticks,setEnabled,setTicks}, the exact _buildLoopControl() state shape. */
|
|
225
|
+
var LOOP_KEYS = Object.freeze(['enabled', 'ticks', 'setEnabled', 'setTicks']);
|
|
226
|
+
|
|
227
|
+
/** ENUM — select-one from a closed candidate set (design §2.2 — census #6:
|
|
228
|
+
* collision_type/gateType/reposition). {id,label,values,value,fmt,onChange,bind}.
|
|
229
|
+
* `value` MUST be a member of `values` — enforced by `_validateEnum`, not listed
|
|
230
|
+
* here (this table is the REQUIRED-KEY list, membership is a separate check). */
|
|
231
|
+
var ENUM_KEYS = Object.freeze(['label', 'values', 'value', 'fmt', 'onChange']);
|
|
232
|
+
|
|
233
|
+
/** FLAGS — multi-select from a closed candidate set (design §2.2 — census #6:
|
|
234
|
+
* the `actions` enum-set field). {id,label,values,value,fmt,onChange,bind}.
|
|
235
|
+
* `value` is an ARRAY; every element MUST be a member of `values` — enforced
|
|
236
|
+
* by `_validateFlags`. */
|
|
237
|
+
var FLAGS_KEYS = Object.freeze(['label', 'values', 'value', 'fmt', 'onChange']);
|
|
238
|
+
|
|
239
|
+
/** VECTOR — an ordered numeric tuple written WHOLE (design §2.2 — census #4/#5:
|
|
240
|
+
* placement/drag, rgb). {id,label,value,min,max,step,fmt,onChange,bind}.
|
|
241
|
+
* `min`/`max`/`step` are each a shared number OR a per-component array the
|
|
242
|
+
* SAME LENGTH as `value` — enforced by `_validateVector`. */
|
|
243
|
+
var VECTOR_KEYS = Object.freeze(['label', 'value', 'min', 'max', 'step', 'fmt', 'onChange']);
|
|
244
|
+
|
|
245
|
+
/** TEXT — a string-valued control (Item 2). {id,label,value,fmt,onChange,bind}. */
|
|
246
|
+
var TEXT_KEYS = Object.freeze(['label', 'value', 'fmt', 'onChange']);
|
|
247
|
+
|
|
248
|
+
/** ACTION — a momentary verb (Item 2). {id,label,action}; `action` ∈ ACTIONS.
|
|
249
|
+
* Stateless: no `value`, no `fmt`, no write-authority pair (the verb IS the write). */
|
|
250
|
+
var ACTION_KEYS = Object.freeze(['label', 'action']);
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* REQUIRED_KEYS_BY_KIND — kind name → its kind-specific required-key list.
|
|
254
|
+
* The SINGLE map the conformance test drives off (iterates KINDS, looks this up —
|
|
255
|
+
* never a parallel switch/if-chain re-deriving which keys belong to which kind).
|
|
256
|
+
*/
|
|
257
|
+
var REQUIRED_KEYS_BY_KIND = Object.freeze({
|
|
258
|
+
scalar: SCALAR_KEYS,
|
|
259
|
+
camera: CAMERA_KEYS,
|
|
260
|
+
loop: LOOP_KEYS,
|
|
261
|
+
enum: ENUM_KEYS,
|
|
262
|
+
flags: FLAGS_KEYS,
|
|
263
|
+
vector: VECTOR_KEYS,
|
|
264
|
+
text: TEXT_KEYS,
|
|
265
|
+
action: ACTION_KEYS
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* BIND_KEYS — required keys on a scalar/enum/flags/vector descriptor's optional
|
|
270
|
+
* `bind` object (WS gym-controls-as-sugar binds+ControlSync FINAL design,
|
|
271
|
+
* converged 2026-07-04: "Declarative binds + triggered sync"; extended to
|
|
272
|
+
* enum/flags/vector by S1 — bind's DATA SHAPE is the SAME `{object, prop}` pair regardless
|
|
273
|
+
* of kind, e.g. a `vector` bind on a LargeObject's `rect` writes the WHOLE
|
|
274
|
+
* tuple in one accessor call, never a torn per-component write). `bind:
|
|
275
|
+
* {object, prop}` is the DATA-SHAPE half of the contract — this module
|
|
276
|
+
* validates the shape ONLY (both non-empty strings); it does NOT resolve
|
|
277
|
+
* `object` or execute `prop` against any accessor. Execution (deriving
|
|
278
|
+
* `get()`/`set()` through the property-accessor substrate, per-channel/per-
|
|
279
|
+
* handle-kind dispatch) is OBJECTS-SIDE — it lives with the sugar that
|
|
280
|
+
* already has the registry/accessor references
|
|
281
|
+
* (`lib/frqtl/parts/FrqtlControlsPanel.js`, `controlBind.js`, and
|
|
282
|
+
* `frqtl-control-sync.js`), never here (arrow ruling: shim owns the DATA
|
|
283
|
+
* shape only). Bind's DATA SHAPE is uniform across channels AND kinds —
|
|
284
|
+
* `channel` is a SIBLING field on the descriptor, already validated above; a
|
|
285
|
+
* render-channel bind still carries `{object, prop}` here, even though the
|
|
286
|
+
* render channel has no read-back (objects-side execution detail, not a shape
|
|
287
|
+
* distinction — see the runtime doc's "non-uniform get()" ruling).
|
|
288
|
+
*/
|
|
289
|
+
var BIND_KEYS = Object.freeze(['object', 'prop']);
|
|
290
|
+
|
|
291
|
+
// ── Validation helpers ───────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function _isFn(v) { return typeof v === 'function'; }
|
|
294
|
+
function _isNum(v) { return typeof v === 'number' && isFinite(v); }
|
|
295
|
+
function _isStr(v) { return typeof v === 'string' && v.length > 0; }
|
|
296
|
+
function _isArr(v) { return Array.isArray(v); }
|
|
297
|
+
function _isBool(v) { return typeof v === 'boolean'; }
|
|
298
|
+
|
|
299
|
+
function _fail(id, kind, msg) {
|
|
300
|
+
throw new Error('[lib/shim/controls] normalizeControlDescriptor(id="' + String(id) +
|
|
301
|
+
'", kind="' + String(kind) + '"): ' + msg + ' (FAIL-LOUD — throw-or-apply, never ' +
|
|
302
|
+
'silently dropped/defaulted-around).');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Validate a descriptor's optional `bind: {object, prop}` — iterates the
|
|
307
|
+
* exported `BIND_KEYS` table (never a hand-copied `if (!bind.object) … if
|
|
308
|
+
* (!bind.prop) …` pair) so a future key added to the bind shape extends this
|
|
309
|
+
* validator automatically. Shared by every bind-capable kind (scalar, enum,
|
|
310
|
+
* flags, vector — S1: generalized from a scalar-only helper, same shape).
|
|
311
|
+
*/
|
|
312
|
+
function _validateBind(id, kind, bind) {
|
|
313
|
+
if (!bind || typeof bind !== 'object') {
|
|
314
|
+
_fail(id, kind, '"bind" must be an object {' + BIND_KEYS.join(', ') + '} when present.');
|
|
315
|
+
}
|
|
316
|
+
for (var i = 0; i < BIND_KEYS.length; i++) {
|
|
317
|
+
var key = BIND_KEYS[i];
|
|
318
|
+
if (!_isStr(bind[key])) {
|
|
319
|
+
_fail(id, kind, '"bind.' + key + '" must be a non-empty string.');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Validate the write-authority rule shared by scalar/enum/flags/vector
|
|
326
|
+
* (converged 2026-07-04, XOR retracted on the 0/9 census of shipped CONTROLS
|
|
327
|
+
* arrays — every one needed a computed onChange, so a bind-replaces-onChange
|
|
328
|
+
* XOR would have made `bind` unusable everywhere): at least ONE of
|
|
329
|
+
* `onChange`/`bind` is required; BOTH may coexist (bind then supplies `get()`
|
|
330
|
+
* for ControlSync even on a control that keeps its computed `onChange` as the
|
|
331
|
+
* mutation path); NEITHER present is a dead control — FAIL-LOUD.
|
|
332
|
+
*/
|
|
333
|
+
function _validateWriteAuthority(id, kind, d) {
|
|
334
|
+
var hasOnChange = d.onChange !== undefined;
|
|
335
|
+
var hasBind = d.bind !== undefined;
|
|
336
|
+
if (!hasOnChange && !hasBind) {
|
|
337
|
+
_fail(id, kind, '"onChange" or "bind" is required — at least ONE write path must be ' +
|
|
338
|
+
'declared (a control with neither is a dead control). Supply onChange (a function) ' +
|
|
339
|
+
'and/or bind ({object,prop}).');
|
|
340
|
+
}
|
|
341
|
+
if (hasOnChange && !_isFn(d.onChange)) {
|
|
342
|
+
_fail(id, kind, '"onChange", when present, must be a function(value)->void.');
|
|
343
|
+
}
|
|
344
|
+
if (hasBind) { _validateBind(id, kind, d.bind); }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Validate a 'scalar' descriptor's kind-specific fields.
|
|
349
|
+
*/
|
|
350
|
+
function _validateScalar(id, d) {
|
|
351
|
+
if (!_isStr(d.label)) { _fail(id, 'scalar', '"label" must be a non-empty string.'); }
|
|
352
|
+
if (!_isNum(d.min)) { _fail(id, 'scalar', '"min" must be a finite number.'); }
|
|
353
|
+
if (!_isNum(d.max)) { _fail(id, 'scalar', '"max" must be a finite number.'); }
|
|
354
|
+
if (!_isNum(d.step)) { _fail(id, 'scalar', '"step" must be a finite number.'); }
|
|
355
|
+
if (typeof d.value !== 'number' || !isFinite(d.value)) {
|
|
356
|
+
_fail(id, 'scalar', '"value" must be a finite number.');
|
|
357
|
+
}
|
|
358
|
+
if (!_isFn(d.fmt)) { _fail(id, 'scalar', '"fmt" must be a function(value)->string.'); }
|
|
359
|
+
_validateWriteAuthority(id, 'scalar', d);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Validate an 'enum' descriptor's kind-specific fields — select-ONE from a
|
|
364
|
+
* closed candidate set (design §2.2 — census #6: collision_type/gateType/
|
|
365
|
+
* reposition). `value` MUST be a member of `values`.
|
|
366
|
+
*/
|
|
367
|
+
function _validateEnum(id, d) {
|
|
368
|
+
if (!_isStr(d.label)) { _fail(id, 'enum', '"label" must be a non-empty string.'); }
|
|
369
|
+
if (!_isArr(d.values) || d.values.length === 0 || !d.values.every(_isStr)) {
|
|
370
|
+
_fail(id, 'enum', '"values" must be a non-empty array of non-empty strings.');
|
|
371
|
+
}
|
|
372
|
+
if (!_isStr(d.value)) { _fail(id, 'enum', '"value" must be a non-empty string.'); }
|
|
373
|
+
if (d.values.indexOf(d.value) === -1) {
|
|
374
|
+
_fail(id, 'enum', '"value" ("' + d.value + '") must be a member of "values" ([' +
|
|
375
|
+
d.values.join(', ') + ']) — enum is select-ONE from a closed candidate set.');
|
|
376
|
+
}
|
|
377
|
+
if (!_isFn(d.fmt)) { _fail(id, 'enum', '"fmt" must be a function(value)->string.'); }
|
|
378
|
+
_validateWriteAuthority(id, 'enum', d);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Validate a 'flags' descriptor's kind-specific fields — multi-select from a
|
|
383
|
+
* closed candidate set (design §2.2 — census #6: the `actions` enum-set
|
|
384
|
+
* field). `value` is an ARRAY; every element MUST be a member of `values`.
|
|
385
|
+
*/
|
|
386
|
+
function _validateFlags(id, d) {
|
|
387
|
+
if (!_isStr(d.label)) { _fail(id, 'flags', '"label" must be a non-empty string.'); }
|
|
388
|
+
if (!_isArr(d.values) || d.values.length === 0 || !d.values.every(_isStr)) {
|
|
389
|
+
_fail(id, 'flags', '"values" must be a non-empty array of non-empty strings.');
|
|
390
|
+
}
|
|
391
|
+
if (!_isArr(d.value) || !d.value.every(_isStr)) {
|
|
392
|
+
_fail(id, 'flags', '"value" must be an array of non-empty strings (may be empty — zero flags set).');
|
|
393
|
+
}
|
|
394
|
+
for (var i = 0; i < d.value.length; i++) {
|
|
395
|
+
if (d.values.indexOf(d.value[i]) === -1) {
|
|
396
|
+
_fail(id, 'flags', '"value[' + i + ']" ("' + d.value[i] + '") must be a member of "values" ([' +
|
|
397
|
+
d.values.join(', ') + ']) — flags is a SUBSET of the closed candidate set.');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!_isFn(d.fmt)) { _fail(id, 'flags', '"fmt" must be a function(value)->string.'); }
|
|
401
|
+
_validateWriteAuthority(id, 'flags', d);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Validate one min/max/step field of a 'vector' descriptor: either a SHARED
|
|
406
|
+
* finite number (applies to every component) or an array of finite numbers
|
|
407
|
+
* the SAME LENGTH as `value` (a per-component bound).
|
|
408
|
+
*/
|
|
409
|
+
function _validateVectorBound(id, key, v, valueLength) {
|
|
410
|
+
if (_isNum(v)) { return; }
|
|
411
|
+
if (_isArr(v) && v.length === valueLength && v.every(_isNum)) { return; }
|
|
412
|
+
_fail(id, 'vector', '"' + key + '" must be a finite number (shared across every component) or an ' +
|
|
413
|
+
'array of ' + valueLength + ' finite number(s) (one per component of "value") — got ' +
|
|
414
|
+
JSON.stringify(v) + '.');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Validate a 'vector' descriptor's kind-specific fields — an ordered numeric
|
|
419
|
+
* tuple written WHOLE (design §2.2 — census #4/#5: placement/drag, rgb).
|
|
420
|
+
* Length is FREE: `value` may be any non-empty array of finite numbers (a
|
|
421
|
+
* 2-tuple x/y, a 4-tuple rect, a 6-tuple 3D rect, a 3-tuple rgb — all the same
|
|
422
|
+
* kind, differing only in `value.length`).
|
|
423
|
+
*/
|
|
424
|
+
function _validateVector(id, d) {
|
|
425
|
+
if (!_isStr(d.label)) { _fail(id, 'vector', '"label" must be a non-empty string.'); }
|
|
426
|
+
if (!_isArr(d.value) || d.value.length === 0 || !d.value.every(_isNum)) {
|
|
427
|
+
_fail(id, 'vector', '"value" must be a non-empty array of finite numbers.');
|
|
428
|
+
}
|
|
429
|
+
_validateVectorBound(id, 'min', d.min, d.value.length);
|
|
430
|
+
_validateVectorBound(id, 'max', d.max, d.value.length);
|
|
431
|
+
_validateVectorBound(id, 'step', d.step, d.value.length);
|
|
432
|
+
if (!_isFn(d.fmt)) { _fail(id, 'vector', '"fmt" must be a function(value)->string.'); }
|
|
433
|
+
_validateWriteAuthority(id, 'vector', d);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Validate a 'text' descriptor's kind-specific fields — a STRING-valued control
|
|
438
|
+
* (Item 2). Same write-authority rule as every other bare-onChange kind.
|
|
439
|
+
*/
|
|
440
|
+
function _validateText(id, d) {
|
|
441
|
+
if (!_isStr(d.label)) { _fail(id, 'text', '"label" must be a non-empty string.'); }
|
|
442
|
+
if (typeof d.value !== 'string') {
|
|
443
|
+
_fail(id, 'text', '"value" must be a string (a text control\'s value; may be empty).');
|
|
444
|
+
}
|
|
445
|
+
if (!_isFn(d.fmt)) { _fail(id, 'text', '"fmt" must be a function(value)->string.'); }
|
|
446
|
+
_validateWriteAuthority(id, 'text', d);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Validate an 'action' descriptor — a momentary verb naming a member of the
|
|
451
|
+
* closed ACTIONS vocabulary. NO write-authority pair: the verb IS the write, and
|
|
452
|
+
* its execution lives objects-side (controlAction.js), never in a closure here.
|
|
453
|
+
*/
|
|
454
|
+
function _validateAction(id, d) {
|
|
455
|
+
if (!_isStr(d.label)) { _fail(id, 'action', '"label" must be a non-empty string.'); }
|
|
456
|
+
if (!_isStr(d.action) || ACTIONS.indexOf(d.action) === -1) {
|
|
457
|
+
_fail(id, 'action', '"action" must be one of [' + ACTIONS.join(', ') + ']; got ' +
|
|
458
|
+
JSON.stringify(d.action) + '.');
|
|
459
|
+
}
|
|
460
|
+
if (d.bind !== undefined) {
|
|
461
|
+
_fail(id, 'action', 'an action descriptor must NOT declare "bind" — an action is a VERB, ' +
|
|
462
|
+
'not a property read/write pair (a get() of "' + d.action + '" is meaningless). ' +
|
|
463
|
+
'Execution is objects-side, by action NAME.');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Validate an optional `place: {x, y, anchor}` — iterates the exported PLACE_KEYS
|
|
469
|
+
* (never a hand-copied per-key if-chain), the SAME discipline as _validateBind.
|
|
470
|
+
* `anchor` is optional here; normalizeControlDescriptor resolves the default.
|
|
471
|
+
*/
|
|
472
|
+
function _validatePlace(id, kind, place) {
|
|
473
|
+
if (!place || typeof place !== 'object' || _isArr(place)) {
|
|
474
|
+
_fail(id, kind, '"place" must be an object {' + PLACE_KEYS.join(', ') + '} when present.');
|
|
475
|
+
}
|
|
476
|
+
for (var i = 0; i < PLACE_KEYS.length; i++) {
|
|
477
|
+
var key = PLACE_KEYS[i];
|
|
478
|
+
var v = place[key];
|
|
479
|
+
if (key === 'anchor') {
|
|
480
|
+
if (v !== undefined && PLACE_ANCHORS.indexOf(v) === -1) {
|
|
481
|
+
_fail(id, kind, '"place.anchor" must be one of [' + PLACE_ANCHORS.join(', ') +
|
|
482
|
+
'] when present; got ' + JSON.stringify(v) + '.');
|
|
483
|
+
}
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (!_isNum(v) || v < 0 || v > 1) {
|
|
487
|
+
_fail(id, kind, '"place.' + key + '" must be a number in [0,1] — a FRACTION of the ' +
|
|
488
|
+
'canvas (resize-stable), not a pixel offset; got ' + JSON.stringify(v) + '.');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Validate a 'camera' descriptor's kind-specific fields. */
|
|
494
|
+
function _validateCamera(id, d) {
|
|
495
|
+
if (!_isArr(d.presets) || !d.presets.every(_isStr)) {
|
|
496
|
+
_fail(id, 'camera', '"presets" must be an array of non-empty strings.');
|
|
497
|
+
}
|
|
498
|
+
if (!_isFn(d.setOrbit)) { _fail(id, 'camera', '"setOrbit" must be a function(spec)->void.'); }
|
|
499
|
+
if (!_isFn(d.getOrbit)) { _fail(id, 'camera', '"getOrbit" must be a function()->{azimuth,elevation,distance}.'); }
|
|
500
|
+
if (!_isFn(d.setPreset)) { _fail(id, 'camera', '"setPreset" must be a function(name)->void.'); }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Validate a 'loop' descriptor's kind-specific fields. */
|
|
504
|
+
function _validateLoop(id, d) {
|
|
505
|
+
if (!_isBool(d.enabled)) { _fail(id, 'loop', '"enabled" must be a boolean.'); }
|
|
506
|
+
if (!_isNum(d.ticks) || d.ticks <= 0) { _fail(id, 'loop', '"ticks" must be a positive finite number.'); }
|
|
507
|
+
if (!_isFn(d.setEnabled)) { _fail(id, 'loop', '"setEnabled" must be a function(boolean)->void.'); }
|
|
508
|
+
if (!_isFn(d.setTicks)) { _fail(id, 'loop', '"setTicks" must be a function(number)->void.'); }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
var _VALIDATORS_BY_KIND = {
|
|
512
|
+
scalar: _validateScalar,
|
|
513
|
+
camera: _validateCamera,
|
|
514
|
+
loop: _validateLoop,
|
|
515
|
+
enum: _validateEnum,
|
|
516
|
+
flags: _validateFlags,
|
|
517
|
+
vector: _validateVector,
|
|
518
|
+
text: _validateText,
|
|
519
|
+
action: _validateAction
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* normalizeControlDescriptor(partial) → full bag.
|
|
524
|
+
*
|
|
525
|
+
* THROW-OR-APPLY (ratified §2 "Normalize semantics"): a malformed descriptor throws
|
|
526
|
+
* here — it is NEVER silently dropped, defaulted-around, or coerced into a
|
|
527
|
+
* different kind. A caller that wants a control MUST supply one that already
|
|
528
|
+
* conforms; there is no "best effort" partial-acceptance path.
|
|
529
|
+
*
|
|
530
|
+
* @param {object} partial — a candidate control descriptor. Must carry:
|
|
531
|
+
* - `kind` — one of KINDS (required).
|
|
532
|
+
* - `id` — non-empty string (required on EVERY kind — the registry
|
|
533
|
+
* addressing key; see lib/shim/controls/registry.js).
|
|
534
|
+
* - `channel` — one of CHANNELS (optional; defaults to DEFAULT_CHANNEL on
|
|
535
|
+
* DEFAULT_CHANNEL_KINDS members ONLY — see the CHANNELS doc
|
|
536
|
+
* comment above).
|
|
537
|
+
* - the kind-specific fields in REQUIRED_KEYS_BY_KIND[kind].
|
|
538
|
+
* @returns {object} a NEW object (the input is never mutated) carrying every
|
|
539
|
+
* input key verbatim plus a resolved `channel`.
|
|
540
|
+
* @throws {Error} FAIL-LOUD on a missing/unrecognized `kind`, a missing/empty
|
|
541
|
+
* `id`, a bad `channel`, or any missing/mistyped kind-specific field.
|
|
542
|
+
*/
|
|
543
|
+
function normalizeControlDescriptor(partial) {
|
|
544
|
+
if (!partial || typeof partial !== 'object') {
|
|
545
|
+
throw new Error('[lib/shim/controls] normalizeControlDescriptor requires a descriptor ' +
|
|
546
|
+
'object; got ' + (partial === null ? 'null' : typeof partial) + '. (FAIL-LOUD.)');
|
|
547
|
+
}
|
|
548
|
+
var kind = partial.kind;
|
|
549
|
+
if (KINDS.indexOf(kind) === -1) {
|
|
550
|
+
throw new Error('[lib/shim/controls] normalizeControlDescriptor: unknown kind "' +
|
|
551
|
+
String(kind) + '" (expect one of [' + KINDS.join(', ') + ']). (FAIL-LOUD.)');
|
|
552
|
+
}
|
|
553
|
+
var id = partial.id;
|
|
554
|
+
if (!_isStr(id)) {
|
|
555
|
+
_fail(id, kind, '"id" must be a non-empty string (every kind requires one — the ' +
|
|
556
|
+
'registry addressing key).');
|
|
557
|
+
}
|
|
558
|
+
var channel = partial.channel;
|
|
559
|
+
if (channel !== undefined && CHANNELS.indexOf(channel) === -1) {
|
|
560
|
+
_fail(id, kind, '"channel" must be one of [' + CHANNELS.join(', ') + '] when present; got "' +
|
|
561
|
+
String(channel) + '".');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Kind-ORTHOGONAL optional fields (Item 2): a widget override and a canvas
|
|
565
|
+
// placement. Validated for EVERY kind, here, exactly once.
|
|
566
|
+
if (partial.widget !== undefined && WIDGETS.indexOf(partial.widget) === -1) {
|
|
567
|
+
_fail(id, kind, '"widget" must be one of [' + WIDGETS.join(', ') + '] when present; got ' +
|
|
568
|
+
JSON.stringify(partial.widget) + '. (widget is kind-ORTHOGONAL — a toggle is a scalar ' +
|
|
569
|
+
'0/1 with widget:"toggle", never a new kind.)');
|
|
570
|
+
}
|
|
571
|
+
if (partial.place !== undefined) { _validatePlace(id, kind, partial.place); }
|
|
572
|
+
|
|
573
|
+
_VALIDATORS_BY_KIND[kind](id, partial);
|
|
574
|
+
|
|
575
|
+
// Build a FRESH bag — never mutate the caller's object (mirrors
|
|
576
|
+
// frame-data's normalizeDataBubble / normalizeRenderIntent convention).
|
|
577
|
+
var bag = {};
|
|
578
|
+
for (var k in partial) {
|
|
579
|
+
if (Object.prototype.hasOwnProperty.call(partial, k)) { bag[k] = partial[k]; }
|
|
580
|
+
}
|
|
581
|
+
bag.kind = kind;
|
|
582
|
+
bag.id = id;
|
|
583
|
+
// channel resolution: DEFAULT_CHANNEL applies ONLY to DEFAULT_CHANNEL_KINDS
|
|
584
|
+
// members when omitted; camera/loop carry whatever was declared (or
|
|
585
|
+
// remain undefined — channel is optional metadata for them, never forced,
|
|
586
|
+
// per the doc comment).
|
|
587
|
+
if (channel === undefined) {
|
|
588
|
+
bag.channel = (DEFAULT_CHANNEL_KINDS.indexOf(kind) !== -1) ? DEFAULT_CHANNEL : undefined;
|
|
589
|
+
} else {
|
|
590
|
+
bag.channel = channel;
|
|
591
|
+
}
|
|
592
|
+
// place: carried as a FRESH object with the anchor default resolved, so every
|
|
593
|
+
// host reads a complete placement (no per-host default re-derivation).
|
|
594
|
+
if (partial.place !== undefined) {
|
|
595
|
+
bag.place = {
|
|
596
|
+
x: partial.place.x,
|
|
597
|
+
y: partial.place.y,
|
|
598
|
+
anchor: (partial.place.anchor !== undefined) ? partial.place.anchor : DEFAULT_PLACE_ANCHOR
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return bag;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
605
|
+
|
|
606
|
+
export {
|
|
607
|
+
KINDS,
|
|
608
|
+
CHANNELS,
|
|
609
|
+
ACTIONS,
|
|
610
|
+
PLACE_KEYS,
|
|
611
|
+
PLACE_ANCHORS,
|
|
612
|
+
DEFAULT_PLACE_ANCHOR,
|
|
613
|
+
WIDGETS,
|
|
614
|
+
TEXT_KEYS,
|
|
615
|
+
ACTION_KEYS,
|
|
616
|
+
DEFAULT_CHANNEL,
|
|
617
|
+
DEFAULT_CHANNEL_KINDS,
|
|
618
|
+
SCALAR_KEYS,
|
|
619
|
+
CAMERA_KEYS,
|
|
620
|
+
LOOP_KEYS,
|
|
621
|
+
ENUM_KEYS,
|
|
622
|
+
FLAGS_KEYS,
|
|
623
|
+
VECTOR_KEYS,
|
|
624
|
+
REQUIRED_KEYS_BY_KIND,
|
|
625
|
+
BIND_KEYS,
|
|
626
|
+
normalizeControlDescriptor
|
|
627
|
+
};
|