@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.2
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/LICENSE.md +664 -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 +2499 -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 +84 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +99 -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 +258 -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 +2470 -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 +2920 -0
- package/dist/frqtl/objects/ObjectFactory.js +296 -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 +710 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -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 +411 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +351 -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 +309 -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 +820 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +468 -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 +250 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -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 +277 -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 +1258 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
- package/dist/frqtl/objects/v1-replacements.js +29 -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 +34 -0
- package/dist/frqtl/parts/CollisionTypes.js +30 -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 +489 -0
- package/dist/frqtl/parts/LargeObject.js +405 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +717 -0
- package/dist/frqtl/parts/OvenBox.js +611 -0
- package/dist/frqtl/parts/PointSource.js +762 -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 +243 -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 +211 -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 +690 -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 +325 -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 +671 -0
- package/dist/shim/notebook/knob.js +150 -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 +507 -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 +213 -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 +804 -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 +63 -4
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* makeControlsForObject.js — the schema→controls FACTORY (Studio Controls
|
|
5
|
+
* Realignment S2, "the factory spine").
|
|
6
|
+
*
|
|
7
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* `makeControlsForObject(wrapper, ctx, opts)` → `{ kind, objectName, controls, display }`.
|
|
10
|
+
*
|
|
11
|
+
* Walks `ObjectSchema.get(kind).fields` (the property-bag authority,
|
|
12
|
+
* `lib/frqtl/objects/schema/`) and mechanically emits ONE contract-legal control
|
|
13
|
+
* descriptor per editable field (`lib/shim/controls` — the control-surface
|
|
14
|
+
* authority) — never a hand-mirrored per-object control array. Each generated
|
|
15
|
+
* control carries `bind:{object, prop}` (a `channel:'physics'` object bind,
|
|
16
|
+
* resolved at execution time by `controlBind.js`'s `resolveBind`, this file's
|
|
17
|
+
* sibling) — this factory NEVER synthesizes an `onChange` closure (see the
|
|
18
|
+
* "no-onChange-synthesis" scope note below; it is a hard constraint, not an
|
|
19
|
+
* oversight, and it is WHY several field types below are DISPLAY-ONLY rather
|
|
20
|
+
* than live controls).
|
|
21
|
+
*
|
|
22
|
+
* @param {*} wrapper — the live object to introspect:
|
|
23
|
+
* - LargeObject/QuantumEmitter/PointSource/DetectorWall/SlitScreen/Box/OvenBox/
|
|
24
|
+
* CircuitGate/CircuitProbe/backdrop wrappers — a JS object carrying `_schemaKind`
|
|
25
|
+
* (kind is read off it automatically; `opts.kind` may still override).
|
|
26
|
+
* - a Frame — NOT an object; per the registry's own documented precedent
|
|
27
|
+
* (`lib/frqtl/objects/objects/controlBind.js` file docstring, "Registration
|
|
28
|
+
* precedent"), a Frame bind target is the bare QUANTUM_ID (a number, from
|
|
29
|
+
* `universe.getFrameQuantumId(handle)`). Pass the qid as `wrapper` and
|
|
30
|
+
* `opts.kind = 'Frame'` (required — a number has no `_schemaKind`).
|
|
31
|
+
* - the Universe singleton — pass ANY truthy placeholder as `wrapper` (unused)
|
|
32
|
+
* and `opts.kind = 'Universe'`; `objectName` resolves to the reserved
|
|
33
|
+
* `'universe'` sentinel automatically (matches `controlBind.js`'s
|
|
34
|
+
* `bind.object === 'universe'` dispatch branch) unless overridden.
|
|
35
|
+
* @param {object} [ctx] — read-context, passed straight through to per-field
|
|
36
|
+
* live-value readers (NOT to `resolveBind` — that happens later, at dispatch
|
|
37
|
+
* time, when a host actually EXECUTES a generated control's bind):
|
|
38
|
+
* - `ctx.universe` — required when `kind === 'Frame'` (`getFrameProperty`).
|
|
39
|
+
* - `ctx.runner` — required when `kind === 'Universe'` (`getUniverseProperty`).
|
|
40
|
+
* Every other kind reads its live value directly off `wrapper[field.name]`
|
|
41
|
+
* (LargeObject/QuantumEmitter/etc. wrappers are plain JS objects with their
|
|
42
|
+
* schema fields as properties — no ctx needed).
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* - `opts.kind` — override/supply the schema kind (REQUIRED for Frame/Universe).
|
|
45
|
+
* - `opts.name` / `opts.prefix` — the registry NAME this object is (or will be)
|
|
46
|
+
* registered under; used to build `id = "<name>.<field>"` and
|
|
47
|
+
* `bind.object = "<name>"`. `opts.prefix` wins if both are supplied.
|
|
48
|
+
* Falls back to `wrapper.name` (LargeObject/etc. often carry a `name` field)
|
|
49
|
+
* then to the `'universe'` sentinel for `kind === 'Universe'`; REQUIRED
|
|
50
|
+
* otherwise (FAIL-LOUD if nothing resolves — an unaddressable id is a
|
|
51
|
+
* factory bug, never silently guessed).
|
|
52
|
+
*
|
|
53
|
+
* @returns {{kind:string, objectName:string,
|
|
54
|
+
* controls:object[], // normalized descriptors — ready for
|
|
55
|
+
* // registry.registerControls(objectName, controls)
|
|
56
|
+
* display:object[]}} // NON-control passthrough entries — see below
|
|
57
|
+
*
|
|
58
|
+
* Every entry in `controls` has ALREADY been run through
|
|
59
|
+
* `normalizeControlDescriptor` (fail-fast: a factory bug throws HERE, at
|
|
60
|
+
* generation time, not later at registration time). `registry.registerControls`
|
|
61
|
+
* re-normalizing them again is harmless (idempotent — normalize only validates
|
|
62
|
+
* + copies) and is how the SAME arrow-legality precedent
|
|
63
|
+
* (`lib/frqtl/objects/schema/index.js` importing `lib/shim/frame-data` directly,
|
|
64
|
+
* see the "Arrow legality" note below) plays out for the controls contract too.
|
|
65
|
+
*
|
|
66
|
+
* `display` entries are NOT control descriptors — they never touch
|
|
67
|
+
* `normalizeControlDescriptor` (there is no "display" kind in `ControlsSchema.KINDS`,
|
|
68
|
+
* and inventing one would violate the closed-union discipline that contract
|
|
69
|
+
* documents). Shape: `{id, label, fieldType, unit, value, description}`. A host
|
|
70
|
+
* (Inspector, panel) renders these as read-only info rows — exactly how
|
|
71
|
+
* `renderFieldControl`'s `'readonly'` branch already renders `<span>{val}</span>`
|
|
72
|
+
* today (`lib/features/ide/AuthorView.jsx`) — this factory just makes the
|
|
73
|
+
* SAME decision mechanically, off the schema, for every kind at once.
|
|
74
|
+
*
|
|
75
|
+
* ── Arrow legality (design §2.1 "Arrow condition") ───────────────────────────
|
|
76
|
+
*
|
|
77
|
+
* This file `require`s `lib/shim/controls/index.js` directly (Node) / reads
|
|
78
|
+
* `window.ControlsSchema` (browser) — an `objects → shim/controls` edge.
|
|
79
|
+
* Checked against `scripts/verify-arrows.mjs`: rule [A] (core-lib-skips-shim)
|
|
80
|
+
* scopes ONLY `lib/features` + `lib/components`; `lib/frqtl/objects` is
|
|
81
|
+
* explicitly annotated there as "user-space sugar — NOT a violation." Rule [B]
|
|
82
|
+
* (engine-sniffing) doesn't apply (no `engine===` branches here). `verify:lib-acyclic`
|
|
83
|
+
* likewise scopes `lib/components`/`lib/features`, not `lib/frqtl/objects`. This
|
|
84
|
+
* exactly mirrors the PRE-EXISTING precedent one directory over —
|
|
85
|
+
* `lib/frqtl/objects/schema/index.js` already `require`s
|
|
86
|
+
* `lib/shim/frame-data/index.js` directly (see that file's own "Imports" comment
|
|
87
|
+
* block) for the identical reason: a schema-contract dependency, not an engine
|
|
88
|
+
* dependency. DEPENDENCY INJECTION (accepting `normalizeControlDescriptor` via
|
|
89
|
+
* `ctx`/`opts` instead of importing it) was the escape hatch IF `verify-arrows`
|
|
90
|
+
* rejected the import — it did not, so this file imports directly, same as its
|
|
91
|
+
* schema/index.js precedent.
|
|
92
|
+
*
|
|
93
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
94
|
+
*
|
|
95
|
+
* Zero physics. This is pure schema-introspection + descriptor-shape glue —
|
|
96
|
+
* generating a control changes NOTHING about the simulation until a host
|
|
97
|
+
* actually calls the generated control's `bind` through `resolveBind` (which
|
|
98
|
+
* itself adds zero physics — see that file's own Layer 2 note). A
|
|
99
|
+
* factory-generated control produces the IDENTICAL effect as hand-writing the
|
|
100
|
+
* same `{kind, id, bind}` literal.
|
|
101
|
+
*
|
|
102
|
+
* ── Scope decisions made explicit here (read before extending) ──────────────
|
|
103
|
+
*
|
|
104
|
+
* 1. NO onChange synthesis (dispatch constraint) — every live control's write
|
|
105
|
+
* path is `bind:{object, prop}` ONLY. This is SAFE for every numeric field
|
|
106
|
+
* (JSON/WASM round-trips a JS number losslessly either way) and safe for
|
|
107
|
+
* every STRING-valued enum/flags field (see #2) — but it is UNSAFE for two
|
|
108
|
+
* field shapes, which are therefore DISPLAY-ONLY, not scalar/
|
|
109
|
+
* enum live controls (documented per-branch below, not silently degraded):
|
|
110
|
+
* (a) a `boolean`-typed field with `engineVisible !== false` — its value
|
|
111
|
+
* reaches the engine ABI via `setFrameProperty`'s
|
|
112
|
+
* `JSON.stringify(value)` (or the WASM LargeObject ABI equivalent);
|
|
113
|
+
* Rust's serde expects a JSON `true`/`false` literal for a `bool`
|
|
114
|
+
* field, not a number — a bare 0/1 scalar-fold passthrough (the
|
|
115
|
+
* shape `SphereEmitter.controls`'s scalar rows use, but THAT control
|
|
116
|
+
* uses an explicit `onChange: v => self.setActive(!!v)` coercion,
|
|
117
|
+
* which this factory is forbidden from synthesizing) risks writing
|
|
118
|
+
* the wrong wire type. `engineVisible:false` booleans (LargeObject/
|
|
119
|
+
* backdrop `.visible`, `.drawAsCircle`) are UNAFFECTED — their write
|
|
120
|
+
* path is a bare `lo[prop] = value` JS assignment (confirmed against
|
|
121
|
+
* every consumer: `p5-provider.js`/`FrqtlApp.hitTest` all
|
|
122
|
+
* check `=== false` or truthiness, never `=== true`) — numeric 0/1
|
|
123
|
+
* is safe there, so those DO become live scalar controls.
|
|
124
|
+
* (b) an `enum`/`enum-set` field whose `constraint.values` is NOT an
|
|
125
|
+
* IDENTITY map (key === value for every key) — e.g.
|
|
126
|
+
* `SlitScreen.detectAtSlits: {NONE:0, DETECT_RANDOM_XY:1, DETECT_MARK:2}`,
|
|
127
|
+
* the ONE such field in the whole schema (verified exhaustively at
|
|
128
|
+
* build time). A bind-driven enum control's `value`/`values` must be
|
|
129
|
+
* STRINGS (`ControlsSchema._validateEnum`); writing the string KEY
|
|
130
|
+
* back through a bare bind would write `'NONE'` into a field the
|
|
131
|
+
* engine expects as the NUMBER `0`. Needs either an onChange
|
|
132
|
+
* translator (forbidden here) or a `resolveBind`-side value-mapping
|
|
133
|
+
* extension — a design decision, not a factory guess.
|
|
134
|
+
* Both are named, tested exceptions — NOT a silent drop. See
|
|
135
|
+
* `_booleanHandler`/`_enumHandler`/`_flagsHandler`/`_writableHandler` below.
|
|
136
|
+
* 2. `rect-array` fields (`QuantumEmitter.source_rects`) become N DISPLAY
|
|
137
|
+
* entries, one per array item — NOT N live vector controls. The `bind`
|
|
138
|
+
* contract (`lib/shim/controls` `BIND_KEYS = ['object','prop']`) has no
|
|
139
|
+
* array-index addressing; a correct per-entry live write needs either
|
|
140
|
+
* `bind.index` (a shim-contract extension) or an onChange closure
|
|
141
|
+
* (forbidden). Flagged per-entry in the `description` field.
|
|
142
|
+
* 3. `QuantumEmitter`/`PointSource`'s singular `rect` field has NO live getter
|
|
143
|
+
* on the wrapper today (a PRE-EXISTING gap — `AuthorView.jsx`'s current
|
|
144
|
+
* Inspector reads `obj['rect']` too and gets the identical `undefined`).
|
|
145
|
+
* `FIELD_VALUE_OVERRIDES` derives a best-effort INITIAL value
|
|
146
|
+
* (`source_rects[0]` / `[x-d/2, y-d/2, d, d]`) so the generated `vector`
|
|
147
|
+
* control still has something to show; when even that isn't derivable, the
|
|
148
|
+
* field is a DEFINED SKIP (no control generated at all) — never a
|
|
149
|
+
* fabricated read (mirrors `controlBind.js`'s own "defined skip" discipline).
|
|
150
|
+
* 4. The "coupled emitter move" op (design §2.2's coder finding — dragging an
|
|
151
|
+
* emitter is TWO writes today, LO rect + emitter sub-rect) has NO factory
|
|
152
|
+
* support yet — TODO for S4: a composite-move op or two coupled `vector`
|
|
153
|
+
* controls sharing one drag gesture. This factory generates each field's
|
|
154
|
+
* control independently; it does not know about cross-field coupling.
|
|
155
|
+
* 5. Behavior-lowering fields (`collision_type`, `actions` — compiled to an
|
|
156
|
+
* op-program at REGISTRATION by `actionCompiler.js`, `engineVisible:false`)
|
|
157
|
+
* DO get live `enum`/`flags` controls here (their values are identity-mapped
|
|
158
|
+
* strings, safe per #1's rule) — but per design §2.3.5, a live edit only
|
|
159
|
+
* mutates the RENDER-layer JS wrapper mirror; it does NOT recompile/repush
|
|
160
|
+
* the running op-program. That gap is the `recompile` write class,
|
|
161
|
+
* explicitly deferred to S4 — this factory does not attempt it.
|
|
162
|
+
* 6. **Bind-executable-kind gate (S3 — MANDATORY, closes an S2-documented KNOWN
|
|
163
|
+
* GAP).** `controlBind.js`'s `resolveBind` only EXECUTES a bind for a
|
|
164
|
+
* registry-resolved kind in `{Frame, LargeObject, backdrop}`, plus the
|
|
165
|
+
* `bind.object === 'universe'` sentinel (this factory's `kind === 'Universe'`)
|
|
166
|
+
* — it FAILS LOUD for every other kind (`QuantumEmitter`, `Box`, `OvenBox`,
|
|
167
|
+
* `DetectorWall`, `SlitScreen`, `CircuitGate`, `CircuitProbe`, `PointSource`,
|
|
168
|
+
* `Tube`, `EgptFractal`, …) because there is no real
|
|
169
|
+
* accessor path from a bare `{object,prop}` pair to that object's actual
|
|
170
|
+
* state — a `QuantumEmitter`'s real state lives in `emState`/`_specTemplate`
|
|
171
|
+
* (mutated only via `setVelocity`/`setBurstSize`/…), and a composite like
|
|
172
|
+
* `Box` exposes CONVENIENCE fields (`width`, `height`, …) that require
|
|
173
|
+
* rebuilding multiple walls, not a bare property set. A host (Studio's
|
|
174
|
+
* Inspector, the page-level panel) that registers this factory's output and
|
|
175
|
+
* resolves binds against it MUST NEVER be handed a control descriptor whose
|
|
176
|
+
* bind is GUARANTEED to throw — an object of a non-bind-executable kind
|
|
177
|
+
* sitting in the scene must not break the whole panel just by existing.
|
|
178
|
+
* So: for ANY kind outside `BIND_EXECUTABLE_KINDS`, every field is demoted
|
|
179
|
+
* to a DISPLAY entry — never a live control — regardless of what its
|
|
180
|
+
* per-field `TYPE_DISPATCH` handler would otherwise produce. This is a
|
|
181
|
+
* per-KIND gate (applied once, after the per-field handler runs), not a
|
|
182
|
+
* per-field one — it sits at the SAME "safe live-write path, or a
|
|
183
|
+
* documented display passthrough" grain as the existing boolean/enum
|
|
184
|
+
* per-field demotions (scope notes #1(a)/#1(b) above), just scoped to the
|
|
185
|
+
* whole object instead of one field. See `_demoteToDisplay` below.
|
|
186
|
+
*
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
import { ObjectSchema as _ObjectSchemaNode } from '../schema/index.js';
|
|
190
|
+
import * as _ControlsSchemaNode from '../../../shim/controls/index.js';
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
// ── Nullable-override sentinel (W0-D) ───────────────────────────────────────────
|
|
194
|
+
// A LIVE per-leaf render-intent OVERRIDE field (SphereEmitter.renderShape,
|
|
195
|
+
// .renderColor) defaults to NULL — "no key, byte-identical, scheme-driven default"
|
|
196
|
+
// (the SU1 guarantee: a non-null default would stamp every leaf and move a golden).
|
|
197
|
+
// Its control must still be EDITABLE (the "renderShape/Color is not changeable
|
|
198
|
+
// in the Inspector" defect), so a null-valued live override renders a control seeded
|
|
199
|
+
// with this reserved sentinel meaning "unset / scheme default". The consuming
|
|
200
|
+
// Inspector maps the sentinel BACK to null on write (AuthorView commitField/
|
|
201
|
+
// commitEmitterField), so the descriptor's own vocab (constraint.values) stays the
|
|
202
|
+
// ONE owner and the null-byte-identity is preserved. Exposed on the exported
|
|
203
|
+
// namespace so the host reads ONE definition (never a hand-copied magic string).
|
|
204
|
+
var NULL_ENUM_SENTINEL = '(default)';
|
|
205
|
+
|
|
206
|
+
// ── Imports (lazy-guarded, mirrors controls/registry.js's own precedent) ────
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
// ── Default-bounds policy (dispatch condition: "never emit an unbounded ──────
|
|
214
|
+
// slider and never invent silent magic numbers without documenting them") ─
|
|
215
|
+
//
|
|
216
|
+
// Precedence per numeric field: `widgetConfig.{min,max,step}` (explicit,
|
|
217
|
+
// deliberate author override — the pre-existing `OvenBox.temperature`/
|
|
218
|
+
// `Universe.wavelengthScale` pattern) > `constraint.{min,max}` (schema-
|
|
219
|
+
// declared bound) > the DEFAULT below. Only ~40% of number fields declare a
|
|
220
|
+
// `max` at all (verified at build time) — most physics-pixel/position/velocity
|
|
221
|
+
// fields are genuinely open-ended, so an unbounded-slider default is not
|
|
222
|
+
// acceptable (dispatch condition) and a fixed universal constant would be
|
|
223
|
+
// meaningless across wildly different field magnitudes (a wall's `left` vs. a
|
|
224
|
+
// `windowTicks` count). The chosen default SCALES WITH THE FIELD'S OWN CURRENT
|
|
225
|
+
// VALUE, exactly mirroring the ONE pre-existing precedent for an un-annotated
|
|
226
|
+
// bounded slider in this codebase — `SphereEmitter.controls`'s
|
|
227
|
+
// `burstSize` row (`max: 200`, lib/frqtl/objects/setup/setupSphereEmitter3D.js).
|
|
228
|
+
// DEFAULT_MAX_FLOOR=40 is that SAME constant, reused for consistency, not
|
|
229
|
+
// invented fresh.
|
|
230
|
+
var DEFAULT_MAX_FLOOR = 40;
|
|
231
|
+
|
|
232
|
+
function _isFiniteNum(v) { return typeof v === 'number' && isFinite(v); }
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolve {min, max, step} for a scalar numeric field, per the precedence above.
|
|
236
|
+
* `min` defaults to a SYMMETRIC `-max` when `constraint.min` is not declared
|
|
237
|
+
* (covers signed fields — velocities, positions that may go negative — the
|
|
238
|
+
* majority of the schema's `min`-less number fields, verified at build time).
|
|
239
|
+
*/
|
|
240
|
+
function _resolveBounds(field, value) {
|
|
241
|
+
var c = field.constraint || {};
|
|
242
|
+
var wc = field.widgetConfig || {};
|
|
243
|
+
var base = _isFiniteNum(value) ? Math.abs(value) : 0;
|
|
244
|
+
var max = (typeof wc.max === 'number') ? wc.max
|
|
245
|
+
: (typeof c.max === 'number') ? c.max
|
|
246
|
+
: Math.max(base * 3, DEFAULT_MAX_FLOOR);
|
|
247
|
+
var min = (typeof wc.min === 'number') ? wc.min
|
|
248
|
+
: (typeof c.min === 'number') ? c.min
|
|
249
|
+
: -max;
|
|
250
|
+
var step = (typeof wc.step === 'number') ? wc.step
|
|
251
|
+
: (c.integer ? 1 : Math.max(max / 100, 0.01));
|
|
252
|
+
return { min: min, max: max, step: step };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _label(field) {
|
|
256
|
+
// An optional display `label` overrides the field NAME for the
|
|
257
|
+
// control's shown label WITHOUT renaming the schema field (the codegen/capture
|
|
258
|
+
// key stays `field.name`). e.g. leafDimension → "Emit Dimension", the global
|
|
259
|
+
// shape capability → "Default Shape". Absent → the name, as before (byte-identical).
|
|
260
|
+
var base = (typeof field.label === 'string' && field.label) ? field.label : field.name;
|
|
261
|
+
return field.unit ? (base + ' (' + field.unit + ')') : base;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function _scalarFmt(field) {
|
|
265
|
+
var integer = !!(field.constraint && field.constraint.integer);
|
|
266
|
+
return function (v) { return integer ? String(Math.round(v)) : Number(v).toFixed(2); };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Live-value reading (kind-dependent — see file docstring @param ctx) ─────
|
|
270
|
+
|
|
271
|
+
// FIELD_VALUE_OVERRIDES — see scope note #3 above.
|
|
272
|
+
var FIELD_VALUE_OVERRIDES = {
|
|
273
|
+
QuantumEmitter: {
|
|
274
|
+
rect: function (w) {
|
|
275
|
+
return (w && Array.isArray(w.source_rects) && w.source_rects[0])
|
|
276
|
+
? w.source_rects[0].slice()
|
|
277
|
+
: undefined;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
PointSource: {
|
|
281
|
+
rect: function (w) {
|
|
282
|
+
if (!w || !_isFiniteNum(w.x) || !_isFiniteNum(w.y) || !_isFiniteNum(w.diameter)) { return undefined; }
|
|
283
|
+
var d = w.diameter;
|
|
284
|
+
return [w.x - d / 2, w.y - d / 2, d, d];
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
// SphereEmitter: `rect` is the WRITE-ONLY reposition adapter — its
|
|
288
|
+
// direct getter is undefined (so captureProps skips it; `center` is the captured
|
|
289
|
+
// position authority). Seed the Inspector's live rect vector from `center` so the
|
|
290
|
+
// field still shows/edits a position (edit → updateObjectProperty → setCenter).
|
|
291
|
+
SphereEmitter: {
|
|
292
|
+
rect: function (w) {
|
|
293
|
+
var c = w && w.center;
|
|
294
|
+
if (!Array.isArray(c) || !_isFiniteNum(c[0]) || !_isFiniteNum(c[1])) { return undefined; }
|
|
295
|
+
var D = 24; // marker diameter (matches the ObjectFactory SphereEmitter builder)
|
|
296
|
+
return [c[0] - D / 2, c[1] - D / 2, D, D];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
function _readValue(env, propName) {
|
|
302
|
+
var kind = env.kind, wrapper = env.wrapper, ctx = env.ctx;
|
|
303
|
+
if (kind === 'Frame') {
|
|
304
|
+
if (!ctx.universe || typeof ctx.universe.getFrameProperty !== 'function') {
|
|
305
|
+
throw new Error('[makeControlsForObject] kind="Frame" requires ctx.universe ' +
|
|
306
|
+
'(getFrameProperty) to read live field values. (FAIL-LOUD.)');
|
|
307
|
+
}
|
|
308
|
+
try { return ctx.universe.getFrameProperty(wrapper, propName); }
|
|
309
|
+
catch (e) { return undefined; } // defined skip — mirrors controlBind.js's own discipline
|
|
310
|
+
}
|
|
311
|
+
if (kind === 'Universe') {
|
|
312
|
+
if (!ctx.runner || typeof ctx.runner.getUniverseProperty !== 'function') {
|
|
313
|
+
throw new Error('[makeControlsForObject] kind="Universe" requires ctx.runner ' +
|
|
314
|
+
'(getUniverseProperty) to read live field values. (FAIL-LOUD.)');
|
|
315
|
+
}
|
|
316
|
+
try { return ctx.runner.getUniverseProperty(propName); }
|
|
317
|
+
catch (e) { return undefined; }
|
|
318
|
+
}
|
|
319
|
+
var overrides = FIELD_VALUE_OVERRIDES[kind];
|
|
320
|
+
if (overrides && typeof overrides[propName] === 'function') { return overrides[propName](wrapper); }
|
|
321
|
+
return (wrapper && typeof wrapper === 'object') ? wrapper[propName] : undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Display-passthrough entries (readonly/forbidden/string/object/deferred) ──
|
|
325
|
+
|
|
326
|
+
function _displayEntry(field, env) {
|
|
327
|
+
var value;
|
|
328
|
+
try { value = _readValue(env, field.name); } catch (e) { value = undefined; }
|
|
329
|
+
return {
|
|
330
|
+
kind: 'display',
|
|
331
|
+
entries: [{
|
|
332
|
+
id: env.objectName + '.' + field.name,
|
|
333
|
+
label: _label(field),
|
|
334
|
+
fieldType: field.type,
|
|
335
|
+
unit: field.unit || null,
|
|
336
|
+
value: value,
|
|
337
|
+
description: field.description || ''
|
|
338
|
+
}]
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Per-field-type handlers ──────────────────────────────────────────────────
|
|
343
|
+
// TYPE_DISPATCH is the EXHAUSTIVE map — every field.type value that can appear
|
|
344
|
+
// in ObjectSchema MUST have an entry here (even if that entry's action is a
|
|
345
|
+
// documented display/skip). An unrecognized type throws (see the main loop) —
|
|
346
|
+
// this is the fail-loud gate the conformance suite drives its exhaustiveness
|
|
347
|
+
// check off of (never a hardcoded type list re-derived in the test file).
|
|
348
|
+
|
|
349
|
+
function _numberHandler(field, env) {
|
|
350
|
+
var value = _readValue(env, field.name);
|
|
351
|
+
if (!_isFiniteNum(value)) { return null; } // defined skip — no live value to seed the slider
|
|
352
|
+
var b = _resolveBounds(field, value);
|
|
353
|
+
return {
|
|
354
|
+
kind: 'control',
|
|
355
|
+
descriptor: {
|
|
356
|
+
kind: 'scalar', id: env.objectName + '.' + field.name, label: _label(field),
|
|
357
|
+
min: b.min, max: b.max, step: b.step, value: value,
|
|
358
|
+
fmt: _scalarFmt(field),
|
|
359
|
+
bind: { object: env.objectName, prop: field.name }
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** See scope note #1(a) above for the engineVisible:true branch's rationale. */
|
|
365
|
+
function _booleanHandler(field, env) {
|
|
366
|
+
if (field.engineVisible !== false) { return _displayEntry(field, env); }
|
|
367
|
+
var value = _readValue(env, field.name);
|
|
368
|
+
if (value === undefined || value === null) { return null; }
|
|
369
|
+
return {
|
|
370
|
+
kind: 'control',
|
|
371
|
+
descriptor: {
|
|
372
|
+
kind: 'scalar', id: env.objectName + '.' + field.name, label: _label(field),
|
|
373
|
+
min: 0, max: 1, step: 1, value: value ? 1 : 0,
|
|
374
|
+
fmt: function (v) { return v ? 'true' : 'false'; },
|
|
375
|
+
bind: { object: env.objectName, prop: field.name }
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Every key must equal its own value (see scope note #1(b)). */
|
|
381
|
+
function _isIdentityValuesMap(valuesObj) {
|
|
382
|
+
var keys = Object.keys(valuesObj);
|
|
383
|
+
for (var i = 0; i < keys.length; i++) {
|
|
384
|
+
if (valuesObj[keys[i]] !== keys[i]) { return false; }
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function _enumHandler(field, env) {
|
|
390
|
+
var valuesObj = (field.constraint && field.constraint.values) || {};
|
|
391
|
+
if (!_isIdentityValuesMap(valuesObj)) { return _displayEntry(field, env); }
|
|
392
|
+
var keys = Object.keys(valuesObj);
|
|
393
|
+
var value = _readValue(env, field.name);
|
|
394
|
+
if (typeof value !== 'string' || keys.indexOf(value) === -1) {
|
|
395
|
+
// W0-D: a LIVE enum whose value is null/unset — the
|
|
396
|
+
// SphereEmitter.renderShape override, kept null for byte-identity (SU1) — still
|
|
397
|
+
// renders an EDITABLE control, seeded with the '(default)' sentinel option
|
|
398
|
+
// representing "unset / scheme default". Picking a real key writes the string
|
|
399
|
+
// (the existing updateObjectProperty → applyProp → setRenderShape path); the
|
|
400
|
+
// Inspector maps the sentinel back to null. A NON-live null enum keeps the
|
|
401
|
+
// pre-existing skip (byte-identical — the sentinel only serves nullable LIVE
|
|
402
|
+
// overrides, never a required enum like collision_type which is always valued).
|
|
403
|
+
if (field.live !== true) { return null; }
|
|
404
|
+
return {
|
|
405
|
+
kind: 'control',
|
|
406
|
+
descriptor: {
|
|
407
|
+
kind: 'enum', id: env.objectName + '.' + field.name, label: _label(field),
|
|
408
|
+
values: [NULL_ENUM_SENTINEL].concat(keys), value: NULL_ENUM_SENTINEL,
|
|
409
|
+
fmt: function (v) { return v; },
|
|
410
|
+
bind: { object: env.objectName, prop: field.name }
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
kind: 'control',
|
|
416
|
+
descriptor: {
|
|
417
|
+
kind: 'enum', id: env.objectName + '.' + field.name, label: _label(field),
|
|
418
|
+
values: keys, value: value,
|
|
419
|
+
fmt: function (v) { return v; },
|
|
420
|
+
bind: { object: env.objectName, prop: field.name }
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function _flagsHandler(field, env) {
|
|
426
|
+
var valuesObj = (field.constraint && field.constraint.values) || {};
|
|
427
|
+
if (!_isIdentityValuesMap(valuesObj)) { return _displayEntry(field, env); }
|
|
428
|
+
var keys = Object.keys(valuesObj);
|
|
429
|
+
var value = _readValue(env, field.name);
|
|
430
|
+
if (!Array.isArray(value)) { return null; }
|
|
431
|
+
return {
|
|
432
|
+
kind: 'control',
|
|
433
|
+
descriptor: {
|
|
434
|
+
kind: 'flags', id: env.objectName + '.' + field.name, label: _label(field),
|
|
435
|
+
values: keys, value: value.slice(),
|
|
436
|
+
fmt: function (v) { return Array.isArray(v) ? v.join(', ') : String(v); },
|
|
437
|
+
bind: { object: env.objectName, prop: field.name }
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// The "unset / scheme default" swatch a null-valued LIVE rgb override seeds its
|
|
443
|
+
// editable control with (W0-D2). White = a neutral "no tint" placeholder; the
|
|
444
|
+
// override is genuinely UNSET (scheme-driven fullness→ROYGBIV) until the user picks
|
|
445
|
+
// a color, at which point setRenderColor stamps the chosen [r,g,b]. (A true
|
|
446
|
+
// "return to unset" gesture needs a companion clear affordance — out of the minimal
|
|
447
|
+
// scope.)
|
|
448
|
+
var NULL_RGB_DEFAULT_SWATCH = [255, 255, 255];
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Is `value` acceptable for `field`'s DECLARED type? The predicate is the schema owner's
|
|
452
|
+
* write-time normalizer (`ObjectSchema.normalizeValue`, throw-or-apply) run as a probe —
|
|
453
|
+
* never a second hand-written per-type test here. A read-side seeding decision and a
|
|
454
|
+
* write-side gate must agree by construction, so there is exactly one implementation.
|
|
455
|
+
*/
|
|
456
|
+
function _isValidForField(field, value, kind) {
|
|
457
|
+
// `null`/`undefined` is a legal WRITE everywhere ("unset" — the normalizer passes it
|
|
458
|
+
// through by contract), but it can never SEED a control: there is no live value to
|
|
459
|
+
// show. So the READ-side probe treats it as not-usable and the caller falls to its
|
|
460
|
+
// own unset branch.
|
|
461
|
+
if (value === null || value === undefined) { return false; }
|
|
462
|
+
try { _ObjectSchemaNode.normalizeValue(field, value, kind); return true; }
|
|
463
|
+
catch (e) { return false; }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function _rgbHandler(field, env) {
|
|
467
|
+
var value = _readValue(env, field.name);
|
|
468
|
+
var isValid = _isValidForField(field, value, env.kind);
|
|
469
|
+
if (!isValid) {
|
|
470
|
+
// W0-D2: a LIVE rgb override whose value is null/unset — the
|
|
471
|
+
// SphereEmitter.renderColor override, kept null for byte-identity (SU1) — still
|
|
472
|
+
// renders an EDITABLE color control seeded with the neutral default swatch, so
|
|
473
|
+
// the field is not a dead "–" in the Inspector. Picking a color writes [r,g,b]
|
|
474
|
+
// (updateObjectProperty → applyProp → setRenderColor). A NON-live null rgb keeps
|
|
475
|
+
// the pre-existing skip (byte-identical — same discipline as the enum branch).
|
|
476
|
+
if (field.live !== true) { return null; }
|
|
477
|
+
value = NULL_RGB_DEFAULT_SWATCH;
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
kind: 'control',
|
|
481
|
+
descriptor: {
|
|
482
|
+
kind: 'vector', id: env.objectName + '.' + field.name, label: _label(field),
|
|
483
|
+
value: value.slice(), min: [0, 0, 0], max: [255, 255, 255], step: [1, 1, 1],
|
|
484
|
+
widgetHint: 'color',
|
|
485
|
+
fmt: function (v) { return 'rgb(' + v.map(function (n) { return Math.round(n); }).join(',') + ')'; },
|
|
486
|
+
bind: { object: env.objectName, prop: field.name }
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Fixed component-name ORDER (LO z/depth wire contract — z is THIRD, not last:
|
|
492
|
+
// [left, top, z, w, h, d]). Position components (left/top/z) may be signed;
|
|
493
|
+
// size components (w/h/d) are non-negative — this drives the per-component
|
|
494
|
+
// min/max policy below.
|
|
495
|
+
var RECT4_KEYS = ['left', 'top', 'w', 'h'];
|
|
496
|
+
var RECT6_KEYS = ['left', 'top', 'z', 'w', 'h', 'd'];
|
|
497
|
+
|
|
498
|
+
function _rectComponentBounds(keys, values) {
|
|
499
|
+
var mins = [], maxs = [], steps = [];
|
|
500
|
+
for (var i = 0; i < keys.length; i++) {
|
|
501
|
+
var isSize = (keys[i] === 'w' || keys[i] === 'h' || keys[i] === 'd');
|
|
502
|
+
var v = values[i];
|
|
503
|
+
var m = Math.max(Math.abs(_isFiniteNum(v) ? v : 0) * 3, DEFAULT_MAX_FLOOR);
|
|
504
|
+
maxs.push(m);
|
|
505
|
+
mins.push(isSize ? 0 : -m);
|
|
506
|
+
steps.push(1);
|
|
507
|
+
}
|
|
508
|
+
return { min: mins, max: maxs, step: steps };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* `rect` → `vector`. Tuple length (4 vs 6) is derived from the LIVE current
|
|
513
|
+
* value, NOT a static schema flag — a LargeObject's `.rect` is a plain object
|
|
514
|
+
* `{left,top,w,h}` OR `{left,top,z,w,h,d}` depending on what was actually
|
|
515
|
+
* passed at CONSTRUCTION time (`LargeObject.js`) — genuinely per-INSTANCE, not
|
|
516
|
+
* per-kind. This matches the `vector` kind's own "length is free" contract
|
|
517
|
+
* (`lib/shim/controls/index.js` doc comment).
|
|
518
|
+
*/
|
|
519
|
+
function _rectHandler(field, env) {
|
|
520
|
+
var raw = _readValue(env, field.name);
|
|
521
|
+
var arr = null, keys = RECT4_KEYS;
|
|
522
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
523
|
+
var hasZD = ('z' in raw) && ('d' in raw);
|
|
524
|
+
keys = hasZD ? RECT6_KEYS : RECT4_KEYS;
|
|
525
|
+
arr = keys.map(function (k) { return raw[k]; });
|
|
526
|
+
} else if (Array.isArray(raw)) {
|
|
527
|
+
keys = (raw.length === 6) ? RECT6_KEYS : RECT4_KEYS;
|
|
528
|
+
arr = raw.slice();
|
|
529
|
+
}
|
|
530
|
+
if (!arr || arr.length === 0 || !arr.every(_isFiniteNum)) { return null; } // defined skip
|
|
531
|
+
var b = _rectComponentBounds(keys, arr);
|
|
532
|
+
return {
|
|
533
|
+
kind: 'control',
|
|
534
|
+
descriptor: {
|
|
535
|
+
kind: 'vector', id: env.objectName + '.' + field.name, label: _label(field),
|
|
536
|
+
value: arr, min: b.min, max: b.max, step: b.step,
|
|
537
|
+
fmt: function (v) { return '[' + v.map(function (n) { return Math.round(n); }).join(', ') + ']'; },
|
|
538
|
+
bind: { object: env.objectName, prop: field.name }
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** See scope note #2 above — DISPLAY entries, one per array item, never a live control. */
|
|
544
|
+
function _rectArrayHandler(field, env) {
|
|
545
|
+
var arr = _readValue(env, field.name);
|
|
546
|
+
if (!Array.isArray(arr) || arr.length === 0) { return null; }
|
|
547
|
+
var entries = [];
|
|
548
|
+
for (var i = 0; i < arr.length; i++) {
|
|
549
|
+
var r = arr[i];
|
|
550
|
+
if (!Array.isArray(r)) { continue; }
|
|
551
|
+
entries.push({
|
|
552
|
+
id: env.objectName + '.' + field.name + '[' + i + ']',
|
|
553
|
+
label: _label(field) + ' [' + i + ']',
|
|
554
|
+
fieldType: 'rect-array-entry',
|
|
555
|
+
unit: field.unit || null,
|
|
556
|
+
value: r.slice(),
|
|
557
|
+
description: (field.description || '') +
|
|
558
|
+
' (DISPLAY ONLY — the bind contract {object,prop} has no array-index ' +
|
|
559
|
+
'addressing; a live per-entry write needs either bind.index, a shim-contract ' +
|
|
560
|
+
'extension, or an onChange closure, forbidden this bite. See ' +
|
|
561
|
+
'makeControlsForObject.js\'s module docstring, scope note #2.)'
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return entries.length ? { kind: 'display', entries: entries } : null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function _readonlyHandler(field, env) { return _displayEntry(field, env); }
|
|
568
|
+
function _forbiddenHandler(field, env) { return _displayEntry(field, env); }
|
|
569
|
+
function _stringHandler(field, env) { return _displayEntry(field, env); }
|
|
570
|
+
function _objectHandler(field, env) { return _displayEntry(field, env); }
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* `writable` (Frame kind ONLY) — this schema type is an ACCESS-LEVEL tag, not
|
|
574
|
+
* a value-type tag (unlike 'number'/'boolean'/'string' on every other kind,
|
|
575
|
+
* which ARE value-typed). x/y/z/vx/vy/vz/mass/speed_prob/local_time are
|
|
576
|
+
* numbers; is_alive/is_stationary/wrap are booleans; charge is a string;
|
|
577
|
+
* data is an object — all declared `type:'writable'`. This handler infers the
|
|
578
|
+
* actual JS value type from the LIVE read and re-dispatches accordingly.
|
|
579
|
+
*/
|
|
580
|
+
function _writableHandler(field, env) {
|
|
581
|
+
var value = _readValue(env, field.name);
|
|
582
|
+
if (value === undefined) { return null; } // defined skip
|
|
583
|
+
if (typeof value === 'number') { return _numberHandler(field, env); }
|
|
584
|
+
if (typeof value === 'boolean') {
|
|
585
|
+
// Frame's writable booleans are ALWAYS engineVisible:true and reach the
|
|
586
|
+
// engine ABI via setFrameProperty's JSON.stringify — same coercion risk
|
|
587
|
+
// as the dedicated 'boolean' handler's engineVisible:true branch (scope
|
|
588
|
+
// note #1(a)). DISPLAY ONLY.
|
|
589
|
+
return _displayEntry(field, env);
|
|
590
|
+
}
|
|
591
|
+
// string (charge — no declared candidate-values set to build a safe 'enum'
|
|
592
|
+
// from) or object (the data bubble — an arbitrary bag) — neither has a
|
|
593
|
+
// live-control shape without inventing behavior the schema doesn't declare.
|
|
594
|
+
return _displayEntry(field, env);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
var TYPE_DISPATCH = {
|
|
598
|
+
number: _numberHandler,
|
|
599
|
+
boolean: _booleanHandler,
|
|
600
|
+
enum: _enumHandler,
|
|
601
|
+
'enum-set': _flagsHandler,
|
|
602
|
+
rgb: _rgbHandler,
|
|
603
|
+
rect: _rectHandler,
|
|
604
|
+
'rect-array': _rectArrayHandler,
|
|
605
|
+
readonly: _readonlyHandler,
|
|
606
|
+
forbidden: _forbiddenHandler,
|
|
607
|
+
string: _stringHandler,
|
|
608
|
+
object: _objectHandler,
|
|
609
|
+
writable: _writableHandler
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// ── Bind-executable-kind gate (see module docstring scope note #6) ──────────
|
|
613
|
+
//
|
|
614
|
+
// The EXACT set of kinds `controlBind.js`'s `resolveBind` can execute a bind
|
|
615
|
+
// against: the three registry-resolved kinds it dispatches on
|
|
616
|
+
// (`Frame`, `LargeObject`, `backdrop`) plus `Universe` (this factory's own
|
|
617
|
+
// `kind === 'Universe'` — resolved through the `bind.object === 'universe'`
|
|
618
|
+
// reserved sentinel, never a registry lookup). Kept as a literal, minimal
|
|
619
|
+
// mirror of `resolveBind`'s own three-way dispatch + the sentinel branch —
|
|
620
|
+
// NOT re-derived from `ObjectSchema.kinds()` (that list is open-ended and
|
|
621
|
+
// growing; this one is closed by what `resolveBind` can actually execute).
|
|
622
|
+
var BIND_EXECUTABLE_KINDS = Object.freeze(['Frame', 'LargeObject', 'backdrop', 'Universe']);
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Demote an already-generated 'control' result to a 'display' result — used
|
|
626
|
+
* ONLY when `env.kind` is outside `BIND_EXECUTABLE_KINDS` (scope note #6).
|
|
627
|
+
* Reuses the descriptor's ALREADY-COMPUTED `id`/`label`/`value` (no second
|
|
628
|
+
* live-value read — the handler already did that work; demoting never
|
|
629
|
+
* re-derives it) so the demoted row shows the exact same value a live control
|
|
630
|
+
* would have shown.
|
|
631
|
+
*/
|
|
632
|
+
function _demoteToDisplay(field, env, result) {
|
|
633
|
+
var d = result.descriptor;
|
|
634
|
+
return {
|
|
635
|
+
kind: 'display',
|
|
636
|
+
entries: [{
|
|
637
|
+
id: d.id,
|
|
638
|
+
label: d.label,
|
|
639
|
+
fieldType: field.type,
|
|
640
|
+
unit: field.unit || null,
|
|
641
|
+
value: d.value,
|
|
642
|
+
description: (field.description || '') +
|
|
643
|
+
' (DISPLAY ONLY — kind "' + env.kind + '" is not in controlBind.js\'s bind-execution ' +
|
|
644
|
+
'allowlist {Frame, LargeObject, backdrop, Universe}; resolveBind FAILS LOUD for any ' +
|
|
645
|
+
'other kind rather than silently no-op through an accessor path that would not reach ' +
|
|
646
|
+
'this object\'s real state.)'
|
|
647
|
+
}]
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ── Conditional visibility (Collision LEXICON v2, §1b) ──────────────────────────
|
|
652
|
+
//
|
|
653
|
+
// ONE declarative `visibleWhen` predicate + ONE evaluator — the general substrate for a
|
|
654
|
+
// field that shows only when a SIBLING field holds a value (bounceDirection only for a
|
|
655
|
+
// bounce wall; angle/decoherence only for a random bounce; markTag only when the mark
|
|
656
|
+
// hook is set). Evaluated LIVE against the current captured props (NOT baked at generation
|
|
657
|
+
// time), so picking 'bounce' reveals bounceDirection immediately. Orthogonal to
|
|
658
|
+
// `internal:true` (always-hide): `internal` is a never-show, `visibleWhen` is a
|
|
659
|
+
// conditional-show — the two are never folded. Shape:
|
|
660
|
+
// { field:'collisionBehavior', equals:'bounce' } — sibling equals a value
|
|
661
|
+
// { field:'hooks', includes:'mark' } — sibling enum-set contains a member
|
|
662
|
+
// [ {…}, {…} ] — ARRAY = AND (the nested case)
|
|
663
|
+
function evalVisibleWhen(field, props) {
|
|
664
|
+
var vw = field && field.visibleWhen;
|
|
665
|
+
if (!vw) { return true; }
|
|
666
|
+
var conds = Array.isArray(vw) ? vw : [vw];
|
|
667
|
+
for (var i = 0; i < conds.length; i++) {
|
|
668
|
+
var c = conds[i];
|
|
669
|
+
var v = (props && typeof props === 'object') ? props[c.field] : undefined;
|
|
670
|
+
if (c.equals !== undefined) { if (v !== c.equals) { return false; } }
|
|
671
|
+
else if (c.includes !== undefined) { if (!Array.isArray(v) || v.indexOf(c.includes) === -1) { return false; } }
|
|
672
|
+
// `present` (emission-gate contract §2): the field shows iff the referenced
|
|
673
|
+
// field is set (non-null) — e.g. gateCounter only once emissionCap is declared.
|
|
674
|
+
else if (c.present !== undefined) { if ((v != null) !== !!c.present) { return false; } }
|
|
675
|
+
// `truthy` (R2AX two-axis mirror, 2026-07-26): the field shows iff the referenced
|
|
676
|
+
// field is truthy — e.g. pitchDeg only on a 3D wall (depth > 0; a 2D wall's derived
|
|
677
|
+
// depth is 0, which `present` cannot distinguish). The ONE evaluator, extended
|
|
678
|
+
// additively like `present` before it.
|
|
679
|
+
else if (c.truthy !== undefined) { if (!!v !== !!c.truthy) { return false; } }
|
|
680
|
+
}
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ── The factory ───────────────────────────────────────────────────────────────
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* @see file docstring for the full contract.
|
|
688
|
+
*/
|
|
689
|
+
function makeControlsForObject(wrapper, ctx, opts) {
|
|
690
|
+
ctx = ctx || {};
|
|
691
|
+
opts = opts || {};
|
|
692
|
+
|
|
693
|
+
var ObjectSchemaRef = _ObjectSchemaNode;
|
|
694
|
+
var normalizeControlDescriptor = _ControlsSchemaNode.normalizeControlDescriptor;
|
|
695
|
+
|
|
696
|
+
var kind = opts.kind ||
|
|
697
|
+
(wrapper && typeof wrapper === 'object' && wrapper._schemaKind) ||
|
|
698
|
+
null;
|
|
699
|
+
if (!kind) {
|
|
700
|
+
throw new Error('[makeControlsForObject] cannot determine object kind — pass opts.kind, or a ' +
|
|
701
|
+
'wrapper object with a "_schemaKind" property (every LargeObject/QuantumEmitter/etc. sets ' +
|
|
702
|
+
'this at construction). A bare Frame quantum_id (a number) or the Universe singleton has no ' +
|
|
703
|
+
'"_schemaKind" — opts.kind is REQUIRED for those. (FAIL-LOUD.)');
|
|
704
|
+
}
|
|
705
|
+
var descriptor = ObjectSchemaRef.get(kind);
|
|
706
|
+
if (!descriptor) {
|
|
707
|
+
throw new Error('[makeControlsForObject] ObjectSchema has no descriptor for kind "' + kind +
|
|
708
|
+
'". (FAIL-LOUD.)');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
var objectName = opts.prefix || opts.name ||
|
|
712
|
+
(kind === 'Universe' ? 'universe' : null) ||
|
|
713
|
+
(wrapper && typeof wrapper === 'object' ? wrapper.name : null) ||
|
|
714
|
+
null;
|
|
715
|
+
if (!objectName) {
|
|
716
|
+
throw new Error('[makeControlsForObject] cannot determine the registry object name (used to ' +
|
|
717
|
+
'prefix every generated control id "<objectName>.<field>" and to build bind.object) — pass ' +
|
|
718
|
+
'opts.name (or opts.prefix). (FAIL-LOUD.)');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
var env = { kind: kind, wrapper: wrapper, ctx: ctx, opts: opts, objectName: objectName };
|
|
722
|
+
|
|
723
|
+
var controls = [];
|
|
724
|
+
var display = [];
|
|
725
|
+
|
|
726
|
+
for (var i = 0; i < descriptor.fields.length; i++) {
|
|
727
|
+
var field = descriptor.fields[i];
|
|
728
|
+
// ── Designer-internal fields (W1 tweak T3) ───────────────────────────
|
|
729
|
+
// `internal:true` (the ONE authority, a descriptor flag — NOT a hardcoded
|
|
730
|
+
// field-name list) marks an ENGINE INTERNAL that must never surface as a
|
|
731
|
+
// designer control/display (e.g. LargeObject.program's raw op-program AST,
|
|
732
|
+
// the derived op-program-scope geometry accessors). Skipped entirely here —
|
|
733
|
+
// no control, no display row. Still engineVisible + captured for codegen.
|
|
734
|
+
if (field.internal === true) { continue; }
|
|
735
|
+
var handler = TYPE_DISPATCH[field.type];
|
|
736
|
+
if (!handler) {
|
|
737
|
+
throw new Error('[makeControlsForObject] unmapped field.type "' + field.type + '" on kind "' +
|
|
738
|
+
kind + '" field "' + field.name + '" — every ObjectSchema field.type must have an ' +
|
|
739
|
+
'explicit TYPE_DISPATCH entry in makeControlsForObject.js (even a documented display/skip). ' +
|
|
740
|
+
'(FAIL-LOUD — never a silent drop.)');
|
|
741
|
+
}
|
|
742
|
+
// ── Explicit per-field LIVE attribute ────────────────────────────────
|
|
743
|
+
// `live:false` → a read-only DISPLAY row regardless of type (boot-level /
|
|
744
|
+
// reload-to-change / derived fields — SphereEmitter's is2D, fundamentalDimension,
|
|
745
|
+
// emissionBox, …). captureProps STILL captures the value; the Inspector just
|
|
746
|
+
// renders it read-only. A field with NO `live` attribute keeps the pre-existing
|
|
747
|
+
// behavior EXACTLY (undefined !== false / !== true → both new branches no-op),
|
|
748
|
+
// so every existing kind is byte-identical.
|
|
749
|
+
if (field.live === false) {
|
|
750
|
+
display = display.concat(_displayEntry(field, env).entries);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
var result = handler(field, env);
|
|
754
|
+
if (!result) { continue; } // documented, deliberate skip (e.g. no live value to seed a control)
|
|
755
|
+
// `live:true` EXEMPTS from the bind-executable demotion: the consuming
|
|
756
|
+
// Inspector / cross-context registry writes this field via
|
|
757
|
+
// updateObjectProperty(wrapper, prop, v) (mechanism A → the kind's own
|
|
758
|
+
// updateObjectProperty branch — e.g. SphereEmitter → control setter), NEVER
|
|
759
|
+
// resolveBind, so the bind-executable allowlist does not apply. Without an
|
|
760
|
+
// explicit `live:true`, the pre-existing demotion stands (S3 gate, scope
|
|
761
|
+
// note #6) — this object's kind has no working resolveBind path.
|
|
762
|
+
if (result.kind === 'control' && field.live !== true && BIND_EXECUTABLE_KINDS.indexOf(kind) === -1) {
|
|
763
|
+
result = _demoteToDisplay(field, env, result);
|
|
764
|
+
}
|
|
765
|
+
// Pass `visibleWhen` THROUGH onto the emitted entry (§1b) — a live,
|
|
766
|
+
// sibling-dependent RENDER concern the Inspector evaluates at render time; NOT a
|
|
767
|
+
// generation-time skip (unlike internal:true). Attached AFTER normalization so the
|
|
768
|
+
// control descriptor's own contract is unchanged (normalize never sees it).
|
|
769
|
+
if (result.kind === 'display') {
|
|
770
|
+
if (field.visibleWhen) { result.entries.forEach(function (e) { e.visibleWhen = field.visibleWhen; }); }
|
|
771
|
+
display = display.concat(result.entries);
|
|
772
|
+
} else if (result.kind === 'control') {
|
|
773
|
+
var norm = normalizeControlDescriptor(result.descriptor);
|
|
774
|
+
if (field.visibleWhen) { norm.visibleWhen = field.visibleWhen; }
|
|
775
|
+
controls.push(norm);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return { kind: kind, objectName: objectName, controls: controls, display: display };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
783
|
+
|
|
784
|
+
var MakeControlsForObject = {
|
|
785
|
+
makeControlsForObject: makeControlsForObject,
|
|
786
|
+
// The ONE conditional-visibility evaluator (Collision LEXICON v2, §1b) —
|
|
787
|
+
// the AuthorView Inspector filter calls THIS beside its existing internal-field drop,
|
|
788
|
+
// never a hand-rolled per-field branch.
|
|
789
|
+
evalVisibleWhen: evalVisibleWhen,
|
|
790
|
+
// W0-D: the reserved "unset / scheme default" sentinel a nullable LIVE enum
|
|
791
|
+
// override control is seeded with. The ONE definition — the host (AuthorView
|
|
792
|
+
// Inspector commit) reads THIS to map the sentinel back to null on write, never
|
|
793
|
+
// a hand-copied magic string.
|
|
794
|
+
NULL_ENUM_SENTINEL: NULL_ENUM_SENTINEL,
|
|
795
|
+
// Exposed for the exhaustiveness conformance test — driven off THIS list,
|
|
796
|
+
// never a hand-copied duplicate (schema-mirror discipline).
|
|
797
|
+
HANDLED_FIELD_TYPES: Object.keys(TYPE_DISPATCH),
|
|
798
|
+
// Exposed so a consuming test (or a future host) can assert against the
|
|
799
|
+
// SAME gate list this factory enforces, never a hand-copied duplicate —
|
|
800
|
+
// see scope note #6 / BIND_EXECUTABLE_KINDS above.
|
|
801
|
+
BIND_EXECUTABLE_KINDS: BIND_EXECUTABLE_KINDS,
|
|
802
|
+
// ── Reuse seam for makeControlsForRenderSurface.js (Studio Controls ───────
|
|
803
|
+
// Realignment §7 r2: REUSES makeControlsForObject's
|
|
804
|
+
// TYPE_DISPATCH — the big DRY win, NOT a third hand-parallel conversion).
|
|
805
|
+
// Exposes ONLY the per-field-type handlers a renderCapabilities() descriptor
|
|
806
|
+
// uses (`number`, `enum`, `boolean`, `string`) — deliberately NOT the full
|
|
807
|
+
// TYPE_DISPATCH map: the rest bake in Frame/Universe-specific assumptions (the
|
|
808
|
+
// rect z/d component ordering, the Frame writable-boolean engine-ABI coercion
|
|
809
|
+
// trap in scope note #1(a)). A boolean render capability declares
|
|
810
|
+
// `engineVisible:false` (it is render-only), so `_booleanHandler` emits its 0/1
|
|
811
|
+
// control; a `string` capability (an OPEN-ID asset id) is a display entry, which
|
|
812
|
+
// the render factory does not turn into a control. These handlers call the
|
|
813
|
+
// generic `_readValue` fallback (`wrapper[propName]`) for any `env.kind` outside
|
|
814
|
+
// {'Frame','Universe'}, so `env.kind:'RenderSurface'` with a synthetic
|
|
815
|
+
// single-field `wrapper` reuses them UNMODIFIED.
|
|
816
|
+
RENDER_TYPE_HANDLERS: { number: _numberHandler, enum: _enumHandler, boolean: _booleanHandler, string: _stringHandler }
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
export { MakeControlsForObject };
|
|
820
|
+
|