@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.
Files changed (218) hide show
  1. package/CLAUDE.md +87 -0
  2. package/LICENSE.md +664 -0
  3. package/README.md +84 -2
  4. package/dist/dist-manifest.json +218 -0
  5. package/dist/eyes/index.js +109 -0
  6. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  7. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  8. package/dist/fat/pkg-node/package.json +12 -0
  9. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  10. package/dist/frqtl/compat/boot-node.js +34 -0
  11. package/dist/frqtl/compat/engine.js +127 -0
  12. package/dist/frqtl/compat/package-root.js +24 -0
  13. package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
  14. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  15. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  16. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  17. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  18. package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
  19. package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
  20. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  21. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  22. package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
  23. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  24. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  25. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  26. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  27. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  28. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  30. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  31. package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
  32. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  33. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  34. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  35. package/dist/frqtl/compat/vendor.js +196 -0
  36. package/dist/frqtl/node-entry.js +17 -0
  37. package/dist/frqtl/objects/FrqtlApp.js +2920 -0
  38. package/dist/frqtl/objects/ObjectFactory.js +296 -0
  39. package/dist/frqtl/objects/RecipeContext.js +322 -0
  40. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  41. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  42. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  43. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  44. package/dist/frqtl/objects/instruments/capture.js +710 -0
  45. package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
  46. package/dist/frqtl/objects/kinds-entry.js +14 -0
  47. package/dist/frqtl/objects/kinds.js +190 -0
  48. package/dist/frqtl/objects/objects/Box.js +273 -0
  49. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  50. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  51. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  52. package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
  53. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  54. package/dist/frqtl/objects/objects/LightSource.js +351 -0
  55. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  56. package/dist/frqtl/objects/objects/Tube.js +471 -0
  57. package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
  58. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  59. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  60. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  61. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  62. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  66. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  67. package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
  68. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  69. package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
  70. package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
  71. package/dist/frqtl/objects/objects/tagger.js +228 -0
  72. package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
  73. package/dist/frqtl/objects/schema/index.js +256 -0
  74. package/dist/frqtl/objects/schema/scopes.js +457 -0
  75. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  76. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  77. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  78. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  79. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  80. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  81. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  82. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  83. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
  85. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  86. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  87. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  88. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  89. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  90. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  91. package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
  92. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  94. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  95. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
  96. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  97. package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
  98. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  99. package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
  100. package/dist/frqtl/objects/v1-replacements.js +29 -0
  101. package/dist/frqtl/package-core.js +23 -0
  102. package/dist/frqtl/parts/CameraControls.js +643 -0
  103. package/dist/frqtl/parts/CameraPath.js +575 -0
  104. package/dist/frqtl/parts/CollisionActions.js +34 -0
  105. package/dist/frqtl/parts/CollisionTypes.js +30 -0
  106. package/dist/frqtl/parts/DetectorWall.js +496 -0
  107. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  108. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  109. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  110. package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
  111. package/dist/frqtl/parts/LargeObject.js +405 -0
  112. package/dist/frqtl/parts/LightColor.js +255 -0
  113. package/dist/frqtl/parts/LiveChart.js +285 -0
  114. package/dist/frqtl/parts/LogicGate.js +717 -0
  115. package/dist/frqtl/parts/OvenBox.js +611 -0
  116. package/dist/frqtl/parts/PointSource.js +762 -0
  117. package/dist/frqtl/parts/Rectangle.js +200 -0
  118. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  119. package/dist/frqtl/parts/SlitScreen.js +628 -0
  120. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  121. package/dist/frqtl/parts/Trace.js +508 -0
  122. package/dist/frqtl/parts/Tunnel.js +1057 -0
  123. package/dist/frqtl/parts/Wall.js +1211 -0
  124. package/dist/frqtl/parts/index.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  126. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  127. package/dist/frqtl/scenes/atomic-model.js +88 -0
  128. package/dist/frqtl/scenes/big-bang.js +46 -0
  129. package/dist/frqtl/scenes/blackbody.js +81 -0
  130. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  131. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  132. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  133. package/dist/frqtl/scenes/counting-wall.js +38 -0
  134. package/dist/frqtl/scenes/double-slit.js +31 -0
  135. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  136. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  137. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  138. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
  139. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  140. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  141. package/dist/frqtl/scenes/empty.js +14 -0
  142. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  143. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  144. package/dist/frqtl/scenes/mirror-route.js +140 -0
  145. package/dist/frqtl/scenes/mirror.js +38 -0
  146. package/dist/frqtl/scenes/omni-3d.js +27 -0
  147. package/dist/frqtl/scenes/omni-source.js +22 -0
  148. package/dist/frqtl/scenes/self-interference.js +39 -0
  149. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  150. package/dist/frqtl/scenes/straight-walk.js +22 -0
  151. package/dist/frqtl/scenes/sun.js +39 -0
  152. package/dist/frqtl/scenes/two-source.js +37 -0
  153. package/dist/frqtl/scenes/wave.js +26 -0
  154. package/dist/shim/bundle/frqnb.js +112 -0
  155. package/dist/shim/bundle/index.js +121 -0
  156. package/dist/shim/bundle/indexHtml.js +107 -0
  157. package/dist/shim/bundle/manifest.js +95 -0
  158. package/dist/shim/bundle/zip.js +243 -0
  159. package/dist/shim/bus.js +67 -0
  160. package/dist/shim/controls/index.js +627 -0
  161. package/dist/shim/controls/registry.js +274 -0
  162. package/dist/shim/engine/artifact-shell.js +211 -0
  163. package/dist/shim/engine/boot-phases.js +44 -0
  164. package/dist/shim/engine/engine-stack-boot.js +59 -0
  165. package/dist/shim/engine/router.js +690 -0
  166. package/dist/shim/experiment-frame/index.js +296 -0
  167. package/dist/shim/expr/ast-schema.js +369 -0
  168. package/dist/shim/field-descriptor/index.js +325 -0
  169. package/dist/shim/frame-data/index.js +224 -0
  170. package/dist/shim/index.js +67 -0
  171. package/dist/shim/notebook/canvas-guard.js +82 -0
  172. package/dist/shim/notebook/catalog.js +35 -0
  173. package/dist/shim/notebook/format.js +736 -0
  174. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  175. package/dist/shim/notebook/kernel.js +671 -0
  176. package/dist/shim/notebook/knob.js +150 -0
  177. package/dist/shim/notebook/module-resolver.js +260 -0
  178. package/dist/shim/notebook/notebook-knobs.js +65 -0
  179. package/dist/shim/notebook/produced-bindings.js +67 -0
  180. package/dist/shim/notebook/realm-registry.js +121 -0
  181. package/dist/shim/notebook/realm-transport.js +73 -0
  182. package/dist/shim/notebook/setup-builder.js +507 -0
  183. package/dist/shim/notebook/store.js +159 -0
  184. package/dist/shim/notebook/vendor-importmap.js +134 -0
  185. package/dist/shim/notebook/view-builder.js +189 -0
  186. package/dist/shim/notebook/view-ctx.js +100 -0
  187. package/dist/shim/notebook/view-handle.js +202 -0
  188. package/dist/shim/notebook/warmup.js +102 -0
  189. package/dist/shim/package.json +4 -0
  190. package/dist/shim/page-params/index.js +213 -0
  191. package/dist/shim/project/identity.js +112 -0
  192. package/dist/shim/project/index.js +19 -0
  193. package/dist/shim/project/link.js +402 -0
  194. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  195. package/dist/shim/project/register-node.mjs +17 -0
  196. package/dist/shim/project/specifiers.js +106 -0
  197. package/dist/shim/registry/copy-gate.js +110 -0
  198. package/dist/shim/registry/exhibits.js +455 -0
  199. package/dist/shim/registry/experiments.js +215 -0
  200. package/dist/shim/registry/index.js +19 -0
  201. package/dist/shim/registry/scene-source.js +157 -0
  202. package/dist/shim/registry/walkthrough.js +804 -0
  203. package/dist/shim/render/capture-envelope.js +279 -0
  204. package/dist/shim/render/heat-colormap.js +92 -0
  205. package/dist/shim/render/render-stages.js +855 -0
  206. package/dist/shim/render/sun-look.js +33 -0
  207. package/dist/shim/render-state/apply.js +262 -0
  208. package/dist/shim/render-state/index.js +74 -0
  209. package/dist/shim/rpc/index.js +207 -0
  210. package/dist/shim/runtime/index.js +1023 -0
  211. package/dist/shim/storage/boot.js +69 -0
  212. package/dist/shim/storage/index.js +169 -0
  213. package/dist/shim/storage/local-storage-provider.js +230 -0
  214. package/dist/shim/storage/make-namespace-store.js +132 -0
  215. package/dist/shim/storage/modules.js +249 -0
  216. package/dist/shim/storage/projects.js +150 -0
  217. package/llms.txt +12 -0
  218. 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
+