@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,946 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SphereEmitter — a p5/three-familiar OBJECT-CONSTRUCTOR facing over the
|
|
5
|
+
* existing `setupSphereEmitter3D` setup factory ("Option B"). It exists so a
|
|
6
|
+
* programmer can write the object idiom
|
|
7
|
+
*
|
|
8
|
+
* var sun = new SphereEmitter([x, y], opts).addTo(universe);
|
|
9
|
+
* sun.setCenter([x2, y2]); // reposition live
|
|
10
|
+
* sun.setWavelength(200); // dial the next leaf's mass
|
|
11
|
+
* sun.dispose();
|
|
12
|
+
*
|
|
13
|
+
* — hold the returned object, and have the emitter emit relative to its OWN
|
|
14
|
+
* position — WITHOUT forking a single line of the factory.
|
|
15
|
+
*
|
|
16
|
+
* ── DRY / PURE DELEGATION SHELL (the whole point) ────────────────────────────
|
|
17
|
+
* `setupSphereEmitter3D(universe, opts)` stays the ONE implementation of the
|
|
18
|
+
* sphere emitter (sampling, positioning, the host emit tick-fn, the live control
|
|
19
|
+
* handle, dispose). `SphereEmitter` adds ZERO sampling / positioning / emit /
|
|
20
|
+
* physics logic — `.addTo(universe)` simply calls the factory with
|
|
21
|
+
* `{ center:[x,y], ...opts }` and then WEARS the factory's returned
|
|
22
|
+
* `sphereEmitterControl` as an object face: every live method (`setCenter`,
|
|
23
|
+
* `setWavelength`, `setEmitEveryTicks`, `setActive`, `setPreset`, `setR`,
|
|
24
|
+
* `dispose`, …) is a straight pass-through to that control. There is NO second
|
|
25
|
+
* emit loop, NO re-implemented sampler, NO engine, NO physics here (canonical-
|
|
26
|
+
* bench SU1 — a delegation shell moves no golden). If you ever find yourself
|
|
27
|
+
* copying logic OUT of `setupSphereEmitter3D` into this file, stop and call the
|
|
28
|
+
* factory instead.
|
|
29
|
+
*
|
|
30
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* `new SphereEmitter(position?, opts?)`
|
|
33
|
+
* @param {[number,number]|[number,number,number]|Object} [position] — emission
|
|
34
|
+
* SOURCE POSITION in ABSOLUTE canvas px (`[x,y]` or `[x,y,z]`), the center
|
|
35
|
+
* this emitter seeds its leaves around. Forwarded verbatim to the factory's
|
|
36
|
+
* `options.center`. If the FIRST argument is a plain options object (not an
|
|
37
|
+
* array), it is treated as `opts` and the position defaults to the canvas
|
|
38
|
+
* center (byte-identical to a factory call with no `center`) — so
|
|
39
|
+
* `new SphereEmitter()` and `new SphereEmitter({ preset:'laser' })` both work.
|
|
40
|
+
* @param {Object} [opts] — the SAME options `setupSphereEmitter3D` takes
|
|
41
|
+
* (`preset`, `emissionBoxSide`, `fundamentalDimension`, `leafDimension`,
|
|
42
|
+
* `wavelength`, `chargeMode`, `emissionShell`, `innerR`, `burstSize`,
|
|
43
|
+
* `emitEveryTicks`, `is2D`, `mode`, `R`, `renderColor`, …).
|
|
44
|
+
* Stored as-is; the universe is NOT touched until `.addTo()`. An explicit
|
|
45
|
+
* `position` argument WINS over `opts.center` (the position arg IS the
|
|
46
|
+
* source position — that is the object's whole purpose).
|
|
47
|
+
*
|
|
48
|
+
* `.addTo(universe) → this`
|
|
49
|
+
* Calls `setupSphereEmitter3D(universe, { center:position, ...opts })`, stores
|
|
50
|
+
* the returned `sphereEmitterControl`, generically forwards its every method
|
|
51
|
+
* onto this object (pure pass-through), and returns `this`. Now the emitter is
|
|
52
|
+
* LIVE (its host tick-fn is registered on `universe.experimentTickFunctions`)
|
|
53
|
+
* and the caller holds the object. Throws if called twice on one instance, or
|
|
54
|
+
* if `setupSphereEmitter3D` isn't loaded (FAIL-LOUD — never a silent no-op).
|
|
55
|
+
*
|
|
56
|
+
* Delegated live handle methods (identical signatures + behavior to the
|
|
57
|
+
* factory's control, since they ARE the factory's control functions):
|
|
58
|
+
* `setCenter(xOrArray, y?, z?)` — accepts EITHER the p5-familiar array
|
|
59
|
+
* (`setCenter([x,y])` / `[x,y,z]`) OR the factory's scalar form
|
|
60
|
+
* (`setCenter(x, y, z)`); both spread to the ONE `control.setCenter`.
|
|
61
|
+
* `getCenter() → [x,y,z]`, `setWavelength(v)`, `getWavelength()`,
|
|
62
|
+
* `getMaxWavelength()`, `setEmitEveryTicks(v)`, `getEmitEveryTicks()`,
|
|
63
|
+
* `setActive(bool)`, `setPreset(v)`, `getPreset()`, `setR(v)`, `getR()`,
|
|
64
|
+
* `setBurstSize(v)`, `getBurstSize()`, `setMode(v)`, `getMode()`,
|
|
65
|
+
* `setChargeMode(v)`, `getChargeMode()`, `setEmissionShell(v)`,
|
|
66
|
+
* `getEmissionShell()`, `setInnerR(v)`, `getInnerR()`, `setEmissionBoxSide(v)`,
|
|
67
|
+
* `getEmissionBoxSide()`, `setRenderColor(rgb)`, `getRenderColor()`,
|
|
68
|
+
* `setAxisRange(axis,lo,hi)`, `getAxisRange(axis)`, `getIs2D()`,
|
|
69
|
+
* `getLeafDimension()`, `getFundamentalDimension()`, `dispose()`, … — every
|
|
70
|
+
* function the control exposes is auto-forwarded (future-proof: a new control
|
|
71
|
+
* method is a new facade method with zero edits here).
|
|
72
|
+
*
|
|
73
|
+
* Convenience getters:
|
|
74
|
+
* `emitter.center` → `getCenter()` (or the pre-`addTo` staged position).
|
|
75
|
+
* `emitter.active` → the last `setActive` value (true until set false).
|
|
76
|
+
* `emitter.control` → the underlying `sphereEmitterControl` (escape hatch for
|
|
77
|
+
* `control.controls` / anything not surfaced as a method).
|
|
78
|
+
* `emitter.controls` → the standardized `runner.experimentControls`-compatible
|
|
79
|
+
* descriptor array (a straight ref to `control.controls`).
|
|
80
|
+
*
|
|
81
|
+
* `.dispose()` — removes this emitter's host tick-fn (idempotent — the factory's
|
|
82
|
+
* control.dispose splices by indexOf, so a double-dispose is a safe no-op).
|
|
83
|
+
*
|
|
84
|
+
* ── ESTIMATION BEFORE RUNNING: how long until there is something to see? ─────
|
|
85
|
+
*
|
|
86
|
+
* A reader must be able to ESTIMATE settle time BEFORE the first run — the dogfood that
|
|
87
|
+
* produced this section spent its first attempt on `{"total":0}` at 300 ticks with a
|
|
88
|
+
* detector 350 px away, and had no way to know that was simply too early rather than
|
|
89
|
+
* broken. THE HEURISTIC:
|
|
90
|
+
*
|
|
91
|
+
* ticks ≈ FLIGHT DISTANCE (px) ÷ the preset's EFFECTIVE FORWARD SPEED (px/tick)
|
|
92
|
+
*
|
|
93
|
+
* A quantum always MOVES at ~1 px/tick — but only a `particle-walk` beam spends all of
|
|
94
|
+
* that going forward. Every other preset spreads its directions, so the mean progress
|
|
95
|
+
* ALONG THE AXIS YOU CARE ABOUT is a FRACTION of the speed, and that fraction is what the
|
|
96
|
+
* heuristic divides by. **Effective forward speed, per preset** (MEASURED on the WASM
|
|
97
|
+
* engine: one 400-leaf burst at the preset defaults, free flight for 300 ticks, seed 42;
|
|
98
|
+
* the mean displacement per tick along the preset's own forward axis, and the mean radial
|
|
99
|
+
* displacement per tick):
|
|
100
|
+
*
|
|
101
|
+
* | preset | forward axis | forward px/tick | radial px/tick |
|
|
102
|
+
* |-----------------|----------------|-----------------|----------------|
|
|
103
|
+
* | `particle-walk` | +x (2D and 3D) | 1.00 | 1.00 |
|
|
104
|
+
* | `laser` | +x 2D / +z 3D | 0.99 / 0.98 | 1.00 |
|
|
105
|
+
* | `hemisphere` | +x (2D and 3D) | 0.65 / 0.49 | 1.00 |
|
|
106
|
+
* | `spherical` | — (isotropic) | ~0 (no drift) | 1.00 |
|
|
107
|
+
* | `drum` | — (in-plane) | ~0 (no drift) | 1.00 |
|
|
108
|
+
*
|
|
109
|
+
* For the two ISOTROPIC presets there is no forward axis: the front of the emission
|
|
110
|
+
* expands radially at ~1 px/tick, so a detector at distance d first sees arrivals at
|
|
111
|
+
* roughly d ticks whatever direction it is in. For a DIRECTED preset use the forward
|
|
112
|
+
* column — e.g. the walkthrough's `hemisphere` source in 3D with a plate 350 px downrange:
|
|
113
|
+
* 350 ÷ 0.49 ≈ **700 ticks** before the MEAN of the population reaches it. At 300 ticks
|
|
114
|
+
* the mean has covered ~150 px, which is exactly why that run counted zero.
|
|
115
|
+
*
|
|
116
|
+
* Read these as a random walk's MEAN, never a wavefront: the spread grows with distance,
|
|
117
|
+
* so the earliest arrivals land well before the estimate and the bulk lands well after.
|
|
118
|
+
* The estimate is for "roughly when is there something to look at", which is the question
|
|
119
|
+
* a reader actually has. `orientationDeg` rotates the forward axis without changing the
|
|
120
|
+
* fraction (a measure-preserving rotation), and `emitEveryTicks`/`burstSize` change HOW
|
|
121
|
+
* MANY arrive, never how fast they cross.
|
|
122
|
+
*
|
|
123
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
124
|
+
* ZERO physics (canonical-bench SU1). Every emergent behavior — uniform-
|
|
125
|
+
* direction ball-rejection sampling, the ±1px/tick move-gate, the transverse
|
|
126
|
+
* wave-walk, `wavelength=capacity/mass`, per-emitter `leafDimension`, the
|
|
127
|
+
* velocity-aligned surface ring launch, opposite-charge braiding — is REALIZED
|
|
128
|
+
* ENTIRELY by `setupSphereEmitter3D` + the engine it composes. This object is a
|
|
129
|
+
* naming/ergonomics face only; it cannot and does not change any golden. See
|
|
130
|
+
* `setupSphereEmitter3D.js`'s Layer 2 for the physics.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
import { setupSphereEmitter3D } from '../objects/setup/setupSphereEmitter3D.js';
|
|
134
|
+
import { registerKind } from '../objects/kinds.js';
|
|
135
|
+
import { LargeObject } from './LargeObject.js';
|
|
136
|
+
import { CollisionTypes } from './CollisionTypes.js';
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* FACADE_OPTIONS — the option keys THIS OBJECT owns and the factory beneath it does
|
|
140
|
+
* not: they are extracted here, never forwarded (`addTo` strips them by driving off
|
|
141
|
+
* this list, so the strip and the declaration cannot drift).
|
|
142
|
+
*
|
|
143
|
+
* It is exported because a SECOND surface needs it. The canonical sketch idiom shares
|
|
144
|
+
* ONE bag across the world step and the part step —
|
|
145
|
+
* `exp.createCanvas(W, H, setupSphereEmitter3D.worldCfg(SRC)); exp.addObject('SphereEmitter', SRC);`
|
|
146
|
+
* (walkthrough.js cl1d and its 3D twin) — so `worldCfg` is handed a SUPERSET of the
|
|
147
|
+
* factory's own vocabulary and must tolerate exactly these keys. The factory cannot
|
|
148
|
+
* require this file (the arrow runs object → setup; a back-require is a cycle), so it
|
|
149
|
+
* declares `SKETCH_OPTIONS` and `setup-factory-option-vocabulary.test.mjs` §4 asserts
|
|
150
|
+
* that list equals `setupSphereEmitter3D.OPTIONS ∪ SphereEmitter.FACADE_OPTIONS` —
|
|
151
|
+
* the drift is a CI failure rather than a hand-kept mirror.
|
|
152
|
+
*/
|
|
153
|
+
var FACADE_OPTIONS = ['emissionCap', 'gateCounter'];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {[number,number]|[number,number,number]|Object} [position]
|
|
157
|
+
* @param {Object} [opts]
|
|
158
|
+
*/
|
|
159
|
+
function SphereEmitter(position, opts) {
|
|
160
|
+
this._schemaKind = 'SphereEmitter';
|
|
161
|
+
// First-arg overload: an array is a position; a plain object is opts
|
|
162
|
+
// (position defaults to canvas center → byte-identical factory default).
|
|
163
|
+
if (Array.isArray(position)) {
|
|
164
|
+
this._center = position.slice();
|
|
165
|
+
} else if (position && typeof position === 'object' && opts === undefined) {
|
|
166
|
+
opts = position;
|
|
167
|
+
this._center = null;
|
|
168
|
+
} else {
|
|
169
|
+
this._center = null;
|
|
170
|
+
}
|
|
171
|
+
this._opts = (opts && typeof opts === 'object') ? opts : {};
|
|
172
|
+
this._control = null; // set by addTo — the factory's sphereEmitterControl
|
|
173
|
+
this._setupResult = null; // the factory's full return (top-level dispose lives here)
|
|
174
|
+
this.universe = null;
|
|
175
|
+
this._active = true; // mirror of control state (control exposes no getActive)
|
|
176
|
+
|
|
177
|
+
// ── Declared EMISSION GATE (emissionCap/gateCounter —
|
|
178
|
+
// circulation-emission-gate-contract §2) ──────────────────────────────────
|
|
179
|
+
// The Blackbody-thermostat pattern as DECLARED DATA: `emissionCap` is a
|
|
180
|
+
// self-throttle target — the emitter deactivates when the live population
|
|
181
|
+
// measured by the counting Wall NAMED by `gateCounter` reaches the cap, and
|
|
182
|
+
// reactivates below it. Compiled at CONSTRUCTION into a host tick-fn (see
|
|
183
|
+
// _installEmissionGate in addTo) so the gate is codegen-round-trippable and
|
|
184
|
+
// Restart-safe (the Restart-safe host-tick-fn rule — the gate is a
|
|
185
|
+
// construction side effect of captured descriptor fields, exactly like the
|
|
186
|
+
// ABSORB radiator, NEVER free recipe JS). Validation is throw-or-apply:
|
|
187
|
+
// a cap without a resolvable counter NAME can never work → THROW here.
|
|
188
|
+
// Zero physics (SU1) — pure emission regulation.
|
|
189
|
+
this._emissionCap = undefined;
|
|
190
|
+
this._gateCounter = undefined;
|
|
191
|
+
this._gateTickFn = null;
|
|
192
|
+
this._runner = null; // threaded by the ObjectFactory builder (by-name resolution seam)
|
|
193
|
+
if (this._opts.emissionCap != null) {
|
|
194
|
+
this._emissionCap = _validateEmissionCap(this._opts.emissionCap);
|
|
195
|
+
if (typeof this._opts.gateCounter !== 'string' || !this._opts.gateCounter) {
|
|
196
|
+
throw new Error('[SphereEmitter] emissionCap requires gateCounter — the NAME of the ' +
|
|
197
|
+
'counting Wall (collisionBehavior "nothing" + count hook) whose getLiveCount() is ' +
|
|
198
|
+
'the cap\'s population authority. An unresolved gate silently running uncapped is ' +
|
|
199
|
+
'the FAIL-LOUD violation this contract closes.');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (this._opts.gateCounter != null) {
|
|
203
|
+
if (typeof this._opts.gateCounter !== 'string' || !this._opts.gateCounter) {
|
|
204
|
+
throw new Error('[SphereEmitter] gateCounter must be a non-empty string (the counting Wall\'s name).');
|
|
205
|
+
}
|
|
206
|
+
this._gateCounter = this._opts.gateCounter;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Throw-or-apply validation for emissionCap (schema constraint {min:1, integer:true}).
|
|
212
|
+
* @param {*} v
|
|
213
|
+
* @returns {number}
|
|
214
|
+
*/
|
|
215
|
+
function _validateEmissionCap(v) {
|
|
216
|
+
if (typeof v !== 'number' || !isFinite(v) || v < 1 || Math.floor(v) !== v) {
|
|
217
|
+
throw new Error('[SphereEmitter] emissionCap must be an integer >= 1; got ' + JSON.stringify(v) +
|
|
218
|
+
'. (Throw-or-apply — never a silently ignored cap.)');
|
|
219
|
+
}
|
|
220
|
+
return v;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Register this emitter on `universe` via the ONE factory and wear its
|
|
225
|
+
* control as an object face. Pure delegation — no physics, no second loop.
|
|
226
|
+
* @param {Object} universe - an EGPTUniverse (JS or WASM compat shim).
|
|
227
|
+
* @returns {SphereEmitter} this (for chaining, p5/three-style).
|
|
228
|
+
*/
|
|
229
|
+
SphereEmitter.prototype.addTo = function (universe) {
|
|
230
|
+
if (this._control) {
|
|
231
|
+
throw new Error('[SphereEmitter] addTo() called twice — one SphereEmitter maps to one live emitter. Construct a new SphereEmitter for a second source.');
|
|
232
|
+
}
|
|
233
|
+
var factory = setupSphereEmitter3D;
|
|
234
|
+
|
|
235
|
+
// Build the factory options: the SAME opts, with the object's position
|
|
236
|
+
// as `center` (the position arg WINS over any opts.center). The declared
|
|
237
|
+
// emission-gate fields (emissionCap/gateCounter) are FACADE-owned regulation
|
|
238
|
+
// data — extracted in the constructor, never forwarded to the factory (the
|
|
239
|
+
// factory's emit contract is untouched; SU1).
|
|
240
|
+
var factoryOpts = {};
|
|
241
|
+
for (var k in this._opts) {
|
|
242
|
+
if (!Object.prototype.hasOwnProperty.call(this._opts, k)) { continue; }
|
|
243
|
+
if (FACADE_OPTIONS.indexOf(k) !== -1) { continue; }
|
|
244
|
+
factoryOpts[k] = this._opts[k];
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(this._center)) { factoryOpts.center = this._center.slice(); }
|
|
247
|
+
|
|
248
|
+
var res = factory(universe, factoryOpts);
|
|
249
|
+
this.universe = universe;
|
|
250
|
+
this._setupResult = res;
|
|
251
|
+
this._control = res.sphereEmitterControl;
|
|
252
|
+
|
|
253
|
+
// Generic pure pass-through: forward EVERY control method onto this
|
|
254
|
+
// object (future-proof — a new control method needs no edit here).
|
|
255
|
+
// Methods ALREADY defined on the prototype (setCenter's array overload,
|
|
256
|
+
// setActive's _active mirror, dispose) are skipped so their adaptations win.
|
|
257
|
+
var control = this._control;
|
|
258
|
+
var self = this;
|
|
259
|
+
Object.keys(control).forEach(function (key) {
|
|
260
|
+
if (typeof control[key] === 'function' && typeof self[key] === 'undefined') {
|
|
261
|
+
self[key] = function () { return control[key].apply(control, arguments); };
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Compile the declared emission gate into a host tick-fn (construction-time
|
|
266
|
+
// side effect of the captured emissionCap/gateCounter fields — Restart-safe).
|
|
267
|
+
if (this._emissionCap != null) { this._installEmissionGate(universe); }
|
|
268
|
+
return this;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* ── The compiled EMISSION-GATE host tick-fn (contract §3/§4) ──────────────────
|
|
273
|
+
*
|
|
274
|
+
* Layer 1 (API): registered on `universe.experimentTickFunctions` (the Phase-A
|
|
275
|
+
* `fn(universe)` seam — the SAME seam as the emit tick-fn, the blackbody
|
|
276
|
+
* thermostat, and the ABSORB radiator). Each tick it:
|
|
277
|
+
* 1. LAZILY resolves the counting Wall by NAME on the FIRST tick (handles are
|
|
278
|
+
* registration-fresh and never survive codegen — the name is the reference;
|
|
279
|
+
* lazy resolution makes emitter/counter construction order irrelevant).
|
|
280
|
+
* Unresolved after the first tick → THROW (never a silent uncapped run).
|
|
281
|
+
* 2. Reads the live population through `counter.getLiveCount(universe._lastTickData)`
|
|
282
|
+
* — the ONE population authority (loop-polish B). It never re-scans
|
|
283
|
+
* tickData.objects[...] inline and never reads a host activeQuanta list; the
|
|
284
|
+
* count-hook guard is REUSED by calling getLiveCount (which throws on a
|
|
285
|
+
* non-counting wall), never re-implemented. `_lastTickData` is the documented
|
|
286
|
+
* one-tick-lag cache the ABSORB radiator also samples (Phase A runs pre-tick).
|
|
287
|
+
* 3. Self-throttles via the emitter's OWN setActive: active while live < cap.
|
|
288
|
+
* The cap is re-read from `this._emissionCap` each tick, so a LIVE edit
|
|
289
|
+
* (Inspector → applyProp('emissionCap', v)) applies on the next tick; a cap
|
|
290
|
+
* cleared to null idles the gate (no setActive writes — no re-fighting a
|
|
291
|
+
* manual toggle).
|
|
292
|
+
*
|
|
293
|
+
* Layer 2 (physics): ZERO physics (SU1) — pure emission regulation, exactly the
|
|
294
|
+
* blackbody-thermostat pattern. The population dynamics it regulates are the
|
|
295
|
+
* engine's own.
|
|
296
|
+
*
|
|
297
|
+
* @param {Object} universe
|
|
298
|
+
*/
|
|
299
|
+
SphereEmitter.prototype._installEmissionGate = function (universe) {
|
|
300
|
+
var self = this;
|
|
301
|
+
var counter = null; // resolved lazily on the first tick, cached after
|
|
302
|
+
var gateFn = function emissionGateTick(u) {
|
|
303
|
+
var cap = self._emissionCap;
|
|
304
|
+
if (cap == null) { return; } // cap cleared live → the gate idles
|
|
305
|
+
if (!counter) {
|
|
306
|
+
var runner = self._runner;
|
|
307
|
+
if (!runner || typeof runner.getObjectByName !== 'function') {
|
|
308
|
+
throw new Error('[SphereEmitter] emissionCap gate cannot resolve gateCounter "' +
|
|
309
|
+
self._gateCounter + '" — no runner with getObjectByName() is attached (construct ' +
|
|
310
|
+
'via ObjectFactory/addObject, which threads the runner). FAIL-LOUD, never a ' +
|
|
311
|
+
'silent uncapped run.');
|
|
312
|
+
}
|
|
313
|
+
var target = runner.getObjectByName(self._gateCounter);
|
|
314
|
+
if (!target) {
|
|
315
|
+
throw new Error('[SphereEmitter] emissionCap gate: gateCounter "' + self._gateCounter +
|
|
316
|
+
'" names no live object on the runner — place a counting Wall (collisionBehavior ' +
|
|
317
|
+
'"nothing" + hooks [\'count\']) with that name. FAIL-LOUD, never a silent uncapped run.');
|
|
318
|
+
}
|
|
319
|
+
if (typeof target.getLiveCount !== 'function') {
|
|
320
|
+
throw new Error('[SphereEmitter] emissionCap gate: "' + self._gateCounter +
|
|
321
|
+
'" is not a counting Wall (no getLiveCount). FAIL-LOUD, never a silent uncapped run.');
|
|
322
|
+
}
|
|
323
|
+
counter = target;
|
|
324
|
+
}
|
|
325
|
+
// The count-hook guard is getLiveCount's OWN throw (reused, not re-implemented);
|
|
326
|
+
// _lastTickData is null before the first completed tick → last-sample 0 → active.
|
|
327
|
+
var uni = (u && u.experimentTickFunctions) ? u : self.universe;
|
|
328
|
+
var alive = counter.getLiveCount(uni ? uni._lastTickData : null);
|
|
329
|
+
self.setActive(alive < cap);
|
|
330
|
+
};
|
|
331
|
+
gateFn._emissionGate = true; // countable marker (dispose-parity / no-stack tests)
|
|
332
|
+
universe.experimentTickFunctions.push(gateFn);
|
|
333
|
+
this._gateTickFn = gateFn;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
function _requireLive(self) {
|
|
337
|
+
if (!self._control) {
|
|
338
|
+
throw new Error('[SphereEmitter] not added yet — call .addTo(universe) before driving the emitter.');
|
|
339
|
+
}
|
|
340
|
+
return self._control;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Reposition the emission SOURCE CENTER live (affects the NEXT emitted leaf).
|
|
345
|
+
* Accepts the p5-familiar array form `setCenter([x,y])` / `[x,y,z]` OR the
|
|
346
|
+
* factory's scalar form `setCenter(x, y, z)` — both spread to the ONE
|
|
347
|
+
* `control.setCenter`. Pure pass-through (the factory does the work).
|
|
348
|
+
*/
|
|
349
|
+
SphereEmitter.prototype.setCenter = function (xOrArray, y, z) {
|
|
350
|
+
var control = _requireLive(this);
|
|
351
|
+
if (Array.isArray(xOrArray)) {
|
|
352
|
+
control.setCenter(xOrArray[0], xOrArray[1], xOrArray.length > 2 ? xOrArray[2] : undefined);
|
|
353
|
+
} else {
|
|
354
|
+
control.setCenter(xOrArray, y, z);
|
|
355
|
+
}
|
|
356
|
+
return this;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Toggle emission on/off (delegates to control.setActive; mirrors `_active`
|
|
361
|
+
* locally so the `active` getter can read it — the control exposes no getter).
|
|
362
|
+
*/
|
|
363
|
+
SphereEmitter.prototype.setActive = function (v) {
|
|
364
|
+
var control = _requireLive(this);
|
|
365
|
+
this._active = !!v;
|
|
366
|
+
control.setActive(this._active);
|
|
367
|
+
return this;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* LIVE emission-cap setter (the applyProp write path). Throw-or-apply: an
|
|
372
|
+
* invalid value THROWS; null/undefined CLEARS the cap (the gate tick-fn idles —
|
|
373
|
+
* it stops writing setActive rather than re-fighting a manual toggle). The gate
|
|
374
|
+
* tick-fn re-reads the cap each tick, so the new threshold applies next tick.
|
|
375
|
+
* NOTE: setting a cap live on an emitter constructed WITHOUT one does not build
|
|
376
|
+
* a gate (the gate is a CONSTRUCTION-time compile of the declared field —
|
|
377
|
+
* reconstruct with emissionCap to gain one); the value still captures for
|
|
378
|
+
* round-trip, and the rebuilt emitter gates.
|
|
379
|
+
* @param {number|null} v
|
|
380
|
+
* @returns {SphereEmitter} this
|
|
381
|
+
*/
|
|
382
|
+
SphereEmitter.prototype.setEmissionCap = function (v) {
|
|
383
|
+
this._emissionCap = (v == null) ? undefined : _validateEmissionCap(v);
|
|
384
|
+
return this;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Remove this emitter's host tick-fns — the compiled emission gate (dispose
|
|
389
|
+
* PARITY with its construction-time push, the anti-stack rule the ABSORB
|
|
390
|
+
* radiator's dispose is the template for: without the splice every reinit
|
|
391
|
+
* STACKS a second gate) and the factory's emit tick-fn (control.dispose).
|
|
392
|
+
* Idempotent (indexOf splice — a second call no-ops). Safe pre-addTo (no-op).
|
|
393
|
+
*/
|
|
394
|
+
SphereEmitter.prototype.dispose = function () {
|
|
395
|
+
if (this._gateTickFn && this.universe && Array.isArray(this.universe.experimentTickFunctions)) {
|
|
396
|
+
var i = this.universe.experimentTickFunctions.indexOf(this._gateTickFn);
|
|
397
|
+
if (i !== -1) { this.universe.experimentTickFunctions.splice(i, 1); }
|
|
398
|
+
}
|
|
399
|
+
this._gateTickFn = null;
|
|
400
|
+
if (this._control) { this._control.dispose(); }
|
|
401
|
+
// The marker is this emitter's (attachMarker): it leaves the world with it.
|
|
402
|
+
if (this._marker && typeof this._marker.dispose === 'function') { this._marker.dispose(); }
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* attachMarker(lo) — take ownership of the Design-mode MARKER (the NONE-collision circle
|
|
407
|
+
* LargeObject STUDIO draws at the emission centre). Called ONCE, by the ObjectFactory
|
|
408
|
+
* SphereEmitter builder after `addTo`. The emitter stamps the ONE up-link on it
|
|
409
|
+
* (`lo._owner = this` — so a pick or an aim that lands on the disc resolves to THIS
|
|
410
|
+
* emitter) and keeps the one down-link (`_marker`) it needs to move it and dispose it.
|
|
411
|
+
* Zero physics: the marker carries no frames and collides with nothing.
|
|
412
|
+
* @param {LargeObject} lo @returns {SphereEmitter} this
|
|
413
|
+
*/
|
|
414
|
+
SphereEmitter.prototype.attachMarker = function (lo) {
|
|
415
|
+
if (!lo || typeof lo !== 'object') {
|
|
416
|
+
throw new TypeError('[SphereEmitter.attachMarker] needs the marker LargeObject. (FAIL-LOUD.)');
|
|
417
|
+
}
|
|
418
|
+
if (this._marker) {
|
|
419
|
+
throw new Error('[SphereEmitter.attachMarker] this emitter already owns a marker — one emitter, ' +
|
|
420
|
+
'one marker. (FAIL-LOUD.)');
|
|
421
|
+
}
|
|
422
|
+
this._marker = lo;
|
|
423
|
+
lo._owner = this;
|
|
424
|
+
if (!this._control) {
|
|
425
|
+
throw new Error('[SphereEmitter.attachMarker] addTo() first — the marker follows the live control\'s ' +
|
|
426
|
+
'centre write. (FAIL-LOUD.)');
|
|
427
|
+
}
|
|
428
|
+
this._control.attachMarker(lo); // the follow hangs on the ONE centre write
|
|
429
|
+
return this;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Convenience getters (the object face — no logic, read-through only).
|
|
433
|
+
Object.defineProperty(SphereEmitter.prototype, 'center', {
|
|
434
|
+
get: function () { return this._control ? this._control.getCenter() : (this._center ? this._center.slice() : null); }
|
|
435
|
+
});
|
|
436
|
+
Object.defineProperty(SphereEmitter.prototype, 'active', {
|
|
437
|
+
get: function () { return this._active; }
|
|
438
|
+
});
|
|
439
|
+
Object.defineProperty(SphereEmitter.prototype, 'control', {
|
|
440
|
+
get: function () { return this._control; }
|
|
441
|
+
});
|
|
442
|
+
Object.defineProperty(SphereEmitter.prototype, 'controls', {
|
|
443
|
+
get: function () { return this._control ? this._control.controls : null; }
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ── Descriptor-field GETTERS (captureProps read surface) ─────────────────
|
|
447
|
+
// captureProps(sphereObj, SphereEmitterDescriptor) reads `sphereObj[field.name]`
|
|
448
|
+
// DIRECTLY for every descriptor field (a getter satisfies it). These expose the
|
|
449
|
+
// LIVE resolved value (from the factory's control) so a dragged/edited emitter
|
|
450
|
+
// Saves at its current state — the codegen round-trip authority. Boot-level fields
|
|
451
|
+
// (fundamentalWaveLength) that the control doesn't surface read from the
|
|
452
|
+
// construction opts (they cannot change live). `rect` is a WRITE-ONLY reposition
|
|
453
|
+
// adapter: its getter is deliberately `undefined` so captureProps SKIPS it —
|
|
454
|
+
// `center` is the single captured position authority (do NOT "fix" this to a value).
|
|
455
|
+
function _liveGetter(readFn) {
|
|
456
|
+
return { get: function () { return this._control ? readFn(this._control, this) : undefined; } };
|
|
457
|
+
}
|
|
458
|
+
Object.defineProperty(SphereEmitter.prototype, 'rect', {
|
|
459
|
+
// PR-1 item iii: a REAL `.rect` getter DERIVED from the live emission center (was
|
|
460
|
+
// hardwired undefined). computeEntityBboxForDesign reads this fresh each paint, so a
|
|
461
|
+
// center-only write (carry / drive / programmatic move) no longer leaves a stale Design
|
|
462
|
+
// bbox — rect DERIVES from the one authority (center), never parallel state. D=24 mirrors
|
|
463
|
+
// the ObjectFactory marker diameter + makeControlsForObject's rect-seed. captureProps no
|
|
464
|
+
// longer trips on this: the descriptor's rect field is `writeOnly:true`, so captureProps
|
|
465
|
+
// SKIPS it by the descriptor flag (not by the getter being undefined) — `center` stays the
|
|
466
|
+
// single captured position authority (codegen round-trip unchanged). The write path
|
|
467
|
+
// (rect → setCenter) is still the applyProp adapter (updateObjectProperty).
|
|
468
|
+
get: function () {
|
|
469
|
+
var c = (this._control && typeof this._control.getCenter === 'function')
|
|
470
|
+
? this._control.getCenter() : null;
|
|
471
|
+
if (!Array.isArray(c) || typeof c[0] !== 'number' || typeof c[1] !== 'number') { return undefined; }
|
|
472
|
+
var D = SphereEmitter.MARKER_D, cz = (typeof c[2] === 'number') ? c[2] : 0;
|
|
473
|
+
return { left: c[0] - D / 2, top: c[1] - D / 2, z: cz - D / 2, w: D, h: D, d: D };
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
Object.defineProperty(SphereEmitter.prototype, 'preset', _liveGetter(function (c) { return c.getPreset(); }));
|
|
477
|
+
Object.defineProperty(SphereEmitter.prototype, 'chargeMode', _liveGetter(function (c) { return c.getChargeMode(); }));
|
|
478
|
+
Object.defineProperty(SphereEmitter.prototype, 'burstSize', _liveGetter(function (c) { return c.getBurstSize(); }));
|
|
479
|
+
Object.defineProperty(SphereEmitter.prototype, 'emitEveryTicks', _liveGetter(function (c) { return c.getEmitEveryTicks(); }));
|
|
480
|
+
Object.defineProperty(SphereEmitter.prototype, 'wavelength', _liveGetter(function (c) { return c.getWavelength(); }));
|
|
481
|
+
Object.defineProperty(SphereEmitter.prototype, 'emissionShell', _liveGetter(function (c) { return c.getEmissionShell(); }));
|
|
482
|
+
Object.defineProperty(SphereEmitter.prototype, 'innerR', _liveGetter(function (c) { return c.getInnerR(); }));
|
|
483
|
+
Object.defineProperty(SphereEmitter.prototype, 'renderColor', _liveGetter(function (c) { return c.getRenderColor(); }));
|
|
484
|
+
Object.defineProperty(SphereEmitter.prototype, 'renderShape', _liveGetter(function (c) { return c.getRenderShape(); }));
|
|
485
|
+
Object.defineProperty(SphereEmitter.prototype, 'fundamentalDimension', _liveGetter(function (c) { return c.getFundamentalDimension(); }));
|
|
486
|
+
Object.defineProperty(SphereEmitter.prototype, 'leafDimension', _liveGetter(function (c) { return c.getLeafDimension(); }));
|
|
487
|
+
Object.defineProperty(SphereEmitter.prototype, 'is2D', _liveGetter(function (c) { return c.getIs2D(); }));
|
|
488
|
+
Object.defineProperty(SphereEmitter.prototype, 'orientationDeg', _liveGetter(function (c) { return c.getOrientation(); }));
|
|
489
|
+
Object.defineProperty(SphereEmitter.prototype, 'emissionBox', _liveGetter(function (c) { return c.getEmissionBox(); }));
|
|
490
|
+
// Declared emission-gate fields (captured for codegen round-trip; undefined
|
|
491
|
+
// when uncapped → captureProps SKIPS them → byte-identical legacy scenes).
|
|
492
|
+
Object.defineProperty(SphereEmitter.prototype, 'emissionCap', {
|
|
493
|
+
get: function () { return this._emissionCap; }
|
|
494
|
+
});
|
|
495
|
+
Object.defineProperty(SphereEmitter.prototype, 'gateCounter', {
|
|
496
|
+
get: function () { return this._gateCounter; }
|
|
497
|
+
});
|
|
498
|
+
// Boot-level, not surfaced by the control — read from the construction opts.
|
|
499
|
+
Object.defineProperty(SphereEmitter.prototype, 'fundamentalWaveLength', {
|
|
500
|
+
get: function () { return (this._opts && this._opts.fundamentalWaveLength !== undefined) ? this._opts.fundamentalWaveLength : 64; }
|
|
501
|
+
});
|
|
502
|
+
// ── Live-edit WRITE ROUTER (FrqtlApp.updateObjectProperty) ───────────
|
|
503
|
+
/**
|
|
504
|
+
* Route ONE schema-prop write to the matching factory-control setter (the
|
|
505
|
+
* mechanism-A write path — the Inspector / coupled-move calls this via
|
|
506
|
+
* FrqtlApp.updateObjectProperty, never resolveBind). `rect` is the
|
|
507
|
+
* reposition adapter: a rect write maps rect-CENTER → the emission center.
|
|
508
|
+
* Boot-level/derived fields have NO live setter (reload to change) — a write
|
|
509
|
+
* is a deliberate no-op on the running emitter (never a stale-mirror mutation).
|
|
510
|
+
* @param {string} prop — a SphereEmitter descriptor field name
|
|
511
|
+
* @param {*} value — the new value
|
|
512
|
+
* @returns {SphereEmitter} this
|
|
513
|
+
*/
|
|
514
|
+
SphereEmitter.prototype.applyProp = function (prop, value) {
|
|
515
|
+
var c = this._control;
|
|
516
|
+
if (!c) { return this; }
|
|
517
|
+
switch (prop) {
|
|
518
|
+
case 'rect':
|
|
519
|
+
// Input adapter: rect-centre → the ONE centre write (a 6-element rect carries z).
|
|
520
|
+
if (Array.isArray(value) && value.length === 6) {
|
|
521
|
+
c.setCenter(value[0] + value[3] / 2, value[1] + value[4] / 2, value[2] + value[5] / 2);
|
|
522
|
+
} else if (Array.isArray(value) && value.length === 4) {
|
|
523
|
+
c.setCenter(value[0] + value[2] / 2, value[1] + value[3] / 2);
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
case 'center':
|
|
527
|
+
if (Array.isArray(value)) { c.setCenter(value[0], value[1], value.length > 2 ? value[2] : undefined); }
|
|
528
|
+
break;
|
|
529
|
+
case 'preset': c.setPreset(value); break;
|
|
530
|
+
case 'chargeMode': c.setChargeMode(value); break;
|
|
531
|
+
case 'burstSize': c.setBurstSize(value); break;
|
|
532
|
+
case 'emitEveryTicks': c.setEmitEveryTicks(value); break;
|
|
533
|
+
case 'wavelength': c.setWavelength(value); break;
|
|
534
|
+
case 'emissionShell': c.setEmissionShell(value); break;
|
|
535
|
+
case 'innerR': c.setInnerR(value); break;
|
|
536
|
+
case 'renderColor': c.setRenderColor(value); break;
|
|
537
|
+
case 'renderShape': c.setRenderShape(value); break;
|
|
538
|
+
// LIVE "Emit Dimension" — host-side addDimension + closure flip;
|
|
539
|
+
// next emitted leaf is born at the new per-emitter dimension (M5). See
|
|
540
|
+
// setupSphereEmitter3D.setLeafDimension for the ratchet side effect (S1).
|
|
541
|
+
case 'leafDimension': c.setLeafDimension(value); break;
|
|
542
|
+
// B8 pulsar orientation (deg) — LIVE; sweeps a constrained beam, omni invariant.
|
|
543
|
+
case 'orientationDeg': c.setOrientation(value); break;
|
|
544
|
+
// Emission gate: the cap is LIVE (throw-or-apply; the gate tick-fn re-reads
|
|
545
|
+
// it next tick — truth-in-labeling readers see the new value immediately).
|
|
546
|
+
case 'emissionCap': this.setEmissionCap(value); break;
|
|
547
|
+
// gateCounter is boot-level (live:false — the gate resolves it once on the
|
|
548
|
+
// first tick; captured for round-trip). A write is a deliberate no-op.
|
|
549
|
+
case 'gateCounter': break;
|
|
550
|
+
// Boot-level / derived — no live setter (reload to change). No-op.
|
|
551
|
+
case 'fundamentalWaveLength': case 'fundamentalDimension':
|
|
552
|
+
case 'is2D': case 'emissionBox':
|
|
553
|
+
break;
|
|
554
|
+
default: break;
|
|
555
|
+
}
|
|
556
|
+
return this;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// ── Export ───────────────────────────────────────────────────────────
|
|
560
|
+
SphereEmitter.FACADE_OPTIONS = FACADE_OPTIONS;
|
|
561
|
+
/** The Design marker's edge, px (a D×D×D cube about the emission centre): the ONE constant the
|
|
562
|
+
* ObjectFactory builder constructs it with and the `rect` getter derives from. */
|
|
563
|
+
SphereEmitter.MARKER_D = 24;
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* SphereEmitter.worldCfg(opts) → the world cfg this emitter's physics needs (its wave-walk
|
|
567
|
+
* scale, fundamental dimension, …) for the sketch's co-init call:
|
|
568
|
+
*
|
|
569
|
+
* exp.createCanvas(W, H, SphereEmitter.worldCfg(SRC)); exp.addObject('SphereEmitter', SRC);
|
|
570
|
+
*
|
|
571
|
+
* PURE DELEGATION to the ONE function, setupSphereEmitter3D.worldCfg — byte-identical to calling
|
|
572
|
+
* it (sphere-emitter-facade.test asserts the identity). The setup factory is machinery, not a
|
|
573
|
+
* part, so this static is the published path to it.
|
|
574
|
+
*/
|
|
575
|
+
SphereEmitter.worldCfg = function worldCfg(opts) {
|
|
576
|
+
return setupSphereEmitter3D.worldCfg(opts);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// ── The kind (objects/kinds.js): descriptor, construction defaults, palette, builder ──────────
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* SphereEmitter — the manipulable palette face over the setupSphereEmitter3D
|
|
583
|
+
* setup factory. A SUGAR kind (peer of
|
|
584
|
+
* QuantumEmitter/PointSource — NOT a new engine kind): the ObjectFactory
|
|
585
|
+
* builder composes a LargeObject-visual marker + the factory's host emit
|
|
586
|
+
* tick-fn (SU1 — the visual LO is NONE-collision, non-leaf, zero physics).
|
|
587
|
+
*
|
|
588
|
+
* This descriptor is the SINGLE field authority: captureProps (codegen),
|
|
589
|
+
* makeControlsForObject (the Inspector), and FrqtlApp.updateObjectProperty
|
|
590
|
+
* (the live-edit write path) ALL drive off it — no hand-list anywhere.
|
|
591
|
+
*
|
|
592
|
+
* Per-field `live` attribute (consumed by makeControlsForObject):
|
|
593
|
+
* live:true → a LIVE Inspector control; the write goes through
|
|
594
|
+
* updateObjectProperty(sphereObj, prop, v) → the control setter
|
|
595
|
+
* (mechanism A — NOT resolveBind; the SphereEmitter kind is
|
|
596
|
+
* deliberately outside controlBind's bind-executable allowlist).
|
|
597
|
+
* live:false → a read-only DISPLAY row (boot-level / derived fields that
|
|
598
|
+
* need a reload to change — same class as the renderer-swap
|
|
599
|
+
* reload rule). captureProps STILL captures its VALUE for
|
|
600
|
+
* round-trip; the Inspector just renders it read-only.
|
|
601
|
+
*
|
|
602
|
+
* FIELD SET is COMPLETE w.r.t. every scene-affecting setupSphereEmitter3D opt
|
|
603
|
+
* (a missing opt = lossy codegen). `emissionBoxSide` is subsumed by the
|
|
604
|
+
* captured derived `emissionBox`. There is no init flag: the SKETCH inits the
|
|
605
|
+
* world once (setupSphereEmitter3D.initWorld / the co-init call), so a second emitter on
|
|
606
|
+
* one universe (6A/6B/6C) is simply a second object.
|
|
607
|
+
*/
|
|
608
|
+
var SphereEmitterDescriptor = {
|
|
609
|
+
description: 'A spherical/omni "sun" emitter — uniform-direction ball-rejected emission ' +
|
|
610
|
+
'(preset spherical/laser/drum/particle-walk). The manipulable palette face over the ' +
|
|
611
|
+
'setupSphereEmitter3D setup factory: pure sugar (a NONE-collision visual marker + a host ' +
|
|
612
|
+
'emit tick-fn), zero physics of its own. Drag the marker to move the emission center live; ' +
|
|
613
|
+
'edit burst/rate/wavelength/charge/preset/shell live; dimension/is2D are boot-level (reload).',
|
|
614
|
+
// ── STUDIO UX presentation contract ───────────────────────────────────
|
|
615
|
+
// A sugar object DECLARES its own STUDIO UX.
|
|
616
|
+
// The vocab is owned once at lib/shim/field-descriptor (UX_HANDLE_ZONES /
|
|
617
|
+
// UX_DRAGGABLE_MODES / UX_SHELL_RENDER); this block is read via
|
|
618
|
+
// ObjectSchema.ux('SphereEmitter'). resizable:false — a SphereEmitter has
|
|
619
|
+
// TWO radii (inner+outer); a single-box resize gesture is ambiguous, so
|
|
620
|
+
// it is NOT resizable (individually, per object). draggable 2D (a 3D
|
|
621
|
+
// scene never cursor-drags). handles: ['move'] — it only moves, never scales.
|
|
622
|
+
// shellRender:'design' — the marker paints only while authoring (the
|
|
623
|
+
// default; the "red light bulb" flips a per-instance shellRender override).
|
|
624
|
+
ux: {
|
|
625
|
+
resizable: false,
|
|
626
|
+
draggable: { '2d': true },
|
|
627
|
+
handles: ['move'],
|
|
628
|
+
shellRender: 'design'
|
|
629
|
+
},
|
|
630
|
+
fields: [
|
|
631
|
+
{
|
|
632
|
+
name: 'rect',
|
|
633
|
+
type: 'rect',
|
|
634
|
+
engineVisible: false,
|
|
635
|
+
live: true,
|
|
636
|
+
// PR-1 item iii: the facade's `.rect` getter now returns a REAL rect DERIVED from
|
|
637
|
+
// the live center (so computeEntityBboxForDesign reads fresh — no stale bbox). To
|
|
638
|
+
// keep `center` the SINGLE captured position authority (codegen round-trip), this
|
|
639
|
+
// field is `writeOnly:true` → captureProps SKIPS it BY THE FLAG (not by the getter
|
|
640
|
+
// being undefined). The WRITE path is unchanged: a rect write maps rect-CENTER →
|
|
641
|
+
// the emission center (control.setCenter, via applyProp).
|
|
642
|
+
writeOnly: true,
|
|
643
|
+
description: 'WRITE-ONLY reposition ADAPTER (drag handle). A rect write — from the visual ' +
|
|
644
|
+
'marker\'s coupled-move or a rect edit — maps rect-CENTER → the emission center ' +
|
|
645
|
+
'(control.setCenter). Its READ getter DERIVES a fresh rect from `center` (PR-1 item ' +
|
|
646
|
+
'iii, for the Design bbox); captureProps SKIPS it via writeOnly:true so `center` stays ' +
|
|
647
|
+
'the single captured position authority. Inspector seeds the vector control from ' +
|
|
648
|
+
'`center` via a FIELD_VALUE_OVERRIDE.'
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: 'center',
|
|
652
|
+
type: 'object',
|
|
653
|
+
engineVisible: false,
|
|
654
|
+
live: false,
|
|
655
|
+
description: 'Emission SOURCE POSITION in absolute canvas px as [x,y,z] (the captured ' +
|
|
656
|
+
'position authority — codegen round-trips this, not rect). Edited live via the rect ' +
|
|
657
|
+
'adapter / the visual marker drag; shown read-only here.'
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: 'preset',
|
|
661
|
+
type: 'enum',
|
|
662
|
+
constraint: { values: { spherical: 'spherical', hemisphere: 'hemisphere', laser: 'laser', drum: 'drum', 'particle-walk': 'particle-walk' } },
|
|
663
|
+
engineVisible: false,
|
|
664
|
+
live: true,
|
|
665
|
+
description: 'Velocity-range preset: spherical (full ball / the sun), hemisphere (the SAME ' +
|
|
666
|
+
'ball-rejected sampler with the forward axis halved to [0,R] — 180° forward emission, ' +
|
|
667
|
+
'aimed by orientationDeg), laser (collimated beam), drum (z=0 disk), particle-walk ' +
|
|
668
|
+
'(deterministic +X drift). LIVE (next burst).'
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: 'chargeMode',
|
|
672
|
+
type: 'enum',
|
|
673
|
+
constraint: { values: { positive: 'positive', negative: 'negative', random: 'random' } },
|
|
674
|
+
engineVisible: false,
|
|
675
|
+
live: true,
|
|
676
|
+
description: 'Per-emitted-leaf charge: positive (0), negative (1), or random (per-leaf ±, ' +
|
|
677
|
+
'the braiding that makes self-interference structure). LIVE (affects the next leaf).'
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: 'burstSize',
|
|
681
|
+
type: 'number',
|
|
682
|
+
constraint: { min: 1, integer: true },
|
|
683
|
+
engineVisible: false,
|
|
684
|
+
live: true,
|
|
685
|
+
description: 'Leaves emitted per burst (flux). LIVE (affects the next burst).'
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'emitEveryTicks',
|
|
689
|
+
type: 'number',
|
|
690
|
+
constraint: { min: 1, integer: true },
|
|
691
|
+
engineVisible: false,
|
|
692
|
+
live: true,
|
|
693
|
+
unit: 'ticks',
|
|
694
|
+
description: 'Ticks between bursts (sustained-emission cadence). LIVE (affects the next burst).'
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
// ── Declared EMISSION GATE (the Blackbody-thermostat
|
|
698
|
+
// pattern; circulation-emission-gate-contract §2 field shapes).
|
|
699
|
+
// Sensor = a placeable counting Wall (name in gateCounter); regulation =
|
|
700
|
+
// emitter-side sugar compiled at CONSTRUCTION into a host tick-fn (the
|
|
701
|
+
// Restart-safe host-tick-fn rule — codegen captures these two fields and
|
|
702
|
+
// the reconstructed emitter's constructor rebuilds the gate).
|
|
703
|
+
name: 'emissionCap',
|
|
704
|
+
type: 'number',
|
|
705
|
+
label: 'Population Limit',
|
|
706
|
+
constraint: { min: 1, integer: true },
|
|
707
|
+
widgetConfig: { max: 12800, step: 1 }, // stable slider range (constraint has no max); 2x the cl1d default 6400
|
|
708
|
+
engineVisible: false,
|
|
709
|
+
live: true,
|
|
710
|
+
description: 'Self-throttle target: the emitter deactivates when the live population ' +
|
|
711
|
+
'measured by gateCounter reaches this value and reactivates when it drops below. ' +
|
|
712
|
+
'null/omitted = uncapped (byte-identical to today). LIVE (next tick). Zero physics ' +
|
|
713
|
+
'(SU1) — pure emission regulation, like the blackbody thermostat.'
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: 'gateCounter',
|
|
717
|
+
type: 'string',
|
|
718
|
+
engineVisible: false,
|
|
719
|
+
live: false,
|
|
720
|
+
visibleWhen: { field: 'emissionCap', present: true },
|
|
721
|
+
description: 'The NAME of the counting Wall (collisionBehavior:"nothing" + count hook) ' +
|
|
722
|
+
'whose getLiveCount() is this cap\'s population authority. Resolved lazily by name ' +
|
|
723
|
+
'at tick time (construction-order independent). FAIL-LOUD if absent once ' +
|
|
724
|
+
'emissionCap is set.'
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: 'wavelength',
|
|
728
|
+
type: 'number',
|
|
729
|
+
constraint: { min: 1, integer: true },
|
|
730
|
+
engineVisible: false,
|
|
731
|
+
live: true,
|
|
732
|
+
description: 'Per-leaf wavelength arg → mass=max(1,capacity/wavelength), hence the transverse ' +
|
|
733
|
+
'oscillation period (CLAUDE.md §6 fork). A NO-OP until fundamentalDimension is raised ' +
|
|
734
|
+
'(capacity 1 saturates mass). LIVE (affects the next leaf).'
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: 'emissionShell',
|
|
738
|
+
type: 'enum',
|
|
739
|
+
constraint: { values: { volume: 'volume', surface: 'surface' } },
|
|
740
|
+
engineVisible: false,
|
|
741
|
+
live: true,
|
|
742
|
+
description: 'Emission start topology: volume (seed within the box) or surface (velocity-' +
|
|
743
|
+
'aligned ring launch at innerR — empty center, sharp bands). LIVE (affects the next leaf).'
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: 'innerR',
|
|
747
|
+
type: 'number',
|
|
748
|
+
constraint: { min: 0, integer: true },
|
|
749
|
+
engineVisible: false,
|
|
750
|
+
live: true,
|
|
751
|
+
unit: 'px',
|
|
752
|
+
description: 'Shell radius for emissionShell:surface (0 collapses to the center point; ' +
|
|
753
|
+
'ignored in volume mode). LIVE (affects the next leaf).'
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: 'renderColor',
|
|
757
|
+
type: 'rgb',
|
|
758
|
+
engineVisible: false,
|
|
759
|
+
live: true,
|
|
760
|
+
description: 'Per-leaf render color override [r,g,b] (render-intent data, zero physics); ' +
|
|
761
|
+
'null = colorless (scheme-driven fullness→ROYGBIV). LIVE (affects the next leaf).'
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: 'renderShape',
|
|
765
|
+
type: 'enum',
|
|
766
|
+
constraint: { values: { ball: 'ball', box: 'box', circle: 'circle', cube: 'cube', sphere: 'sphere' } },
|
|
767
|
+
engineVisible: false,
|
|
768
|
+
live: true,
|
|
769
|
+
description: 'Per-leaf render.shape override (render-intent data, zero physics) — peer of ' +
|
|
770
|
+
'renderColor. Stamped into string_map[\'render.shape\'] on each emitted leaf via the ' +
|
|
771
|
+
'provider render-intent MERGE seam. Overrides the render-level "Default Shape" ' +
|
|
772
|
+
'(setRenderOption). null/omitted = fall back to the default. LIVE (affects the next leaf).'
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
// EMITTER ORIENTATION (B8 pulsar) — the CURRENT orientation the velocity sampler
|
|
776
|
+
// rotates each sampled (vx,vy) about z by. Spinning a constrained (laser) emitter
|
|
777
|
+
// sweeps the beam (a pulsar); omni ('spherical') is rotationally invariant. Also the
|
|
778
|
+
// LOOK-drive rotation target. SU1: at 0 the rotation is the identity (byte-identical).
|
|
779
|
+
name: 'orientationDeg',
|
|
780
|
+
type: 'number',
|
|
781
|
+
constraint: { min: 0, max: 360 },
|
|
782
|
+
engineVisible: false,
|
|
783
|
+
live: true,
|
|
784
|
+
widgetHint: 'slider',
|
|
785
|
+
description: 'Emitter orientation in degrees (default 0). The velocity sampler rotates ' +
|
|
786
|
+
'each emitted (vx,vy) by this angle (M5: affects the NEXT leaf), so spinning a ' +
|
|
787
|
+
'constrained/laser emitter sweeps its beam (pulsar); an omni emitter is unaffected. ' +
|
|
788
|
+
'LIVE via control.setOrientation; the LOOK-drive rotation target.'
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: 'fundamentalWaveLength',
|
|
792
|
+
type: 'number',
|
|
793
|
+
constraint: { min: 1, integer: true },
|
|
794
|
+
engineVisible: false,
|
|
795
|
+
live: false,
|
|
796
|
+
description: 'Init-time wavelengthScale seed (waveLengthMultiplier=floor(fwl/4)); boot-level ' +
|
|
797
|
+
'(set at universe.init — reload to change). Captured for round-trip.'
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: 'fundamentalDimension',
|
|
801
|
+
type: 'number',
|
|
802
|
+
constraint: { min: 0, integer: true },
|
|
803
|
+
engineVisible: false,
|
|
804
|
+
live: false,
|
|
805
|
+
description: 'The universe base dimension this emitter inits at; boot-level (reload to change). ' +
|
|
806
|
+
'Raises leaf capacity so the wavelength knob gains leverage. Captured for round-trip.'
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: 'leafDimension',
|
|
810
|
+
type: 'number',
|
|
811
|
+
constraint: { min: 0, integer: true },
|
|
812
|
+
engineVisible: false,
|
|
813
|
+
live: true,
|
|
814
|
+
// Field NAME stays `leafDimension` (codegen/capture key; a rename = round-trip
|
|
815
|
+
// break + a second name = schema mirror — Q-A2). The Inspector shows
|
|
816
|
+
// the display LABEL "Emit Dimension"; the write routes through applyProp →
|
|
817
|
+
// control.setLeafDimension (host-side addDimension + closure var flip; the NEXT
|
|
818
|
+
// emitted leaf is born at the new dimension). Mid-run addDimension is
|
|
819
|
+
// SAFE (cleared 2026-07-22). SIDE EFFECT (S1): a value BELOW the current
|
|
820
|
+
// fundamental floor self-lowers the floor UNIVERSE-GLOBALLY and is a ONE-WAY
|
|
821
|
+
// RATCHET (no removeDimension) — the Inspector surfaces a hint when crossing below
|
|
822
|
+
// floor. fundamentalDimension stays display-only (universe base = reinit-class).
|
|
823
|
+
label: 'Emit Dimension',
|
|
824
|
+
// R2-2: rendered as a SCROLLER LIST (a <select> showing the current dimension +
|
|
825
|
+
// choices), NOT a slider — via the resolveWidget 'dimension-select' widget. The
|
|
826
|
+
// option list is dynamic runtime state sourced from the emitter (fundamental +
|
|
827
|
+
// current leafDimension), so the descriptor stays the single FIELD authority while
|
|
828
|
+
// the candidate values come from the world (not a schema mirror).
|
|
829
|
+
widgetHint: 'dimension-select',
|
|
830
|
+
description: 'The per-emitter dimension leaves are born at (default = fundamentalDimension). ' +
|
|
831
|
+
'LIVE (host-side addDimension + closure flip; next emitted leaf). A leaf born ABOVE ' +
|
|
832
|
+
'the fundamental is move-gated (Invariant 6A) — slower/larger/redder, the per-dimension ' +
|
|
833
|
+
'observable. A value BELOW the current fundamental self-lowers the floor universe-globally ' +
|
|
834
|
+
'(a one-way ratchet). Captured for round-trip.'
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: 'is2D',
|
|
838
|
+
type: 'boolean',
|
|
839
|
+
engineVisible: false,
|
|
840
|
+
live: false,
|
|
841
|
+
description: 'Whether the z-axis is inactive (2D disk of directions vs 3D ball); boot-level ' +
|
|
842
|
+
'(set at init via activeAxes — reload to change). Captured for round-trip.'
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: 'emissionBox',
|
|
846
|
+
type: 'object',
|
|
847
|
+
engineVisible: false,
|
|
848
|
+
live: false,
|
|
849
|
+
description: 'Derived per-axis emission-position box {x,y,z} (subsumes the scalar ' +
|
|
850
|
+
'emissionBoxSide input). Captured for round-trip; frozen when re-passed on reload.'
|
|
851
|
+
}
|
|
852
|
+
]
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
//
|
|
856
|
+
// The manipulable palette face over setupSphereEmitter3D. Mirrors the
|
|
857
|
+
// QuantumEmitter/PointSource pattern: build the emitter, then a NONE-collision
|
|
858
|
+
// circle LargeObject "marker" the emitter OWNS (attachMarker), and return the
|
|
859
|
+
// emitter — the declared object STUDIO picks, drags and carries. SU1:
|
|
860
|
+
// the marker is NONE-collision + not a leaf → pure render/control surface, zero
|
|
861
|
+
// physics. The emitter itself is the SphereEmitter facade (pure delegation over
|
|
862
|
+
// setupSphereEmitter3D). It is a PART: it never inits the world — the SKETCH
|
|
863
|
+
// (the recipe's co-init call / a page's setup factory) did that before any object.
|
|
864
|
+
registerKind(SphereEmitter, {
|
|
865
|
+
from: import.meta.url,
|
|
866
|
+
descriptor: SphereEmitterDescriptor,
|
|
867
|
+
defaults: {
|
|
868
|
+
preset: 'spherical',
|
|
869
|
+
is2D: true
|
|
870
|
+
},
|
|
871
|
+
palette: { displayName: 'Sphere Emitter', category: 'emitter', iconHint: 'emitter', rank: 0 },
|
|
872
|
+
build: function (props, ctx, Class) {
|
|
873
|
+
var SphereEmitterCls = Class; // the class that registered (a composed fork's own class)
|
|
874
|
+
|
|
875
|
+
// Forward the config VERBATIM to the factory (via the facade) — MINUS the declared
|
|
876
|
+
// `name` (the facade's own construction fact). (The write-only `rect` adapter never
|
|
877
|
+
// reaches a builder: create() refuses it as a construction key.) This verbatim forward
|
|
878
|
+
// is the opts-identity guarantee: the emitter sees exactly what the recipe
|
|
879
|
+
// declared (the walkthrough intercept test asserts this tuple-for-tuple).
|
|
880
|
+
var opts = {};
|
|
881
|
+
for (var k in props) {
|
|
882
|
+
if (!Object.prototype.hasOwnProperty.call(props, k)) { continue; }
|
|
883
|
+
if (k === 'name') { continue; }
|
|
884
|
+
opts[k] = props[k];
|
|
885
|
+
}
|
|
886
|
+
// F1 (tester V2 FAIL, 2026-07-23): an interactive palette DROP carries the
|
|
887
|
+
// click-projected world point in ctx.x/ctx.y (buildObject sets fromPalette:true).
|
|
888
|
+
// Thread it into the emitter's `center` (ABSOLUTE canvas px) so the drop lands at
|
|
889
|
+
// the click, mirroring what the LargeObject/QuantumEmitter builders already do —
|
|
890
|
+
// the emitter otherwise self-inits at its default canvas center and the projected
|
|
891
|
+
// point is silently discarded (every drop landed at [300,300,0]). GUARD: only for
|
|
892
|
+
// a real palette drop (fromPalette) AND only when the recipe/opts did NOT already
|
|
893
|
+
// declare placement (center/offset) — so the recipe/rung path (addObject, no
|
|
894
|
+
// fromPalette, relies on the default center) stays BYTE-IDENTICAL and an explicit
|
|
895
|
+
// recipe center/offset still wins (the opts-identity guarantee).
|
|
896
|
+
if (ctx.fromPalette && opts.center == null && opts.offset == null &&
|
|
897
|
+
typeof ctx.x === 'number' && typeof ctx.y === 'number') {
|
|
898
|
+
opts.center = [ctx.x, ctx.y];
|
|
899
|
+
}
|
|
900
|
+
// Construction validates the option bag (its owner's migration messages win) and
|
|
901
|
+
// registers nothing; the marker classes are resolved before addTo registers the
|
|
902
|
+
// emitter, so a missing class never leaves a marker-less emitter in the world.
|
|
903
|
+
var sphere = new SphereEmitterCls(opts);
|
|
904
|
+
sphere._schemaKind = Class.name;
|
|
905
|
+
var LO = LargeObject;
|
|
906
|
+
var CT = CollisionTypes;
|
|
907
|
+
// Thread the runner BEFORE addTo (which compiles the declared emission gate):
|
|
908
|
+
// the gate's lazy by-name gateCounter resolution goes through the runner's
|
|
909
|
+
// canonical resolver (FrqtlApp.getObjectByName) on its first tick.
|
|
910
|
+
sphere._runner = ctx.runner;
|
|
911
|
+
sphere.addTo(ctx.runner.universe);
|
|
912
|
+
|
|
913
|
+
// Visual marker (circle LargeObject, NONE collision) — the drag/inspect handle.
|
|
914
|
+
var center = (typeof sphere.getCenter === 'function') ? sphere.getCenter() : [ctx.x, ctx.y, 0];
|
|
915
|
+
// The marker is z-capable from the start (a D×D×D cube about the centre, d STATED): the
|
|
916
|
+
// emitter re-places it at every centre write (attachMarker), x, y AND z.
|
|
917
|
+
var D = SphereEmitterCls.MARKER_D;
|
|
918
|
+
var cz = (typeof center[2] === 'number') ? center[2] : 0;
|
|
919
|
+
var vRect = [Math.round(center[0] - D / 2), Math.round(center[1] - D / 2), cz - D / 2, D, D, D];
|
|
920
|
+
// The declared NAME is the emitter's own construction fact; its marker wears it.
|
|
921
|
+
sphere.name = props.name || ('sphere emitter ' + (ctx.runner.largeObjects.length));
|
|
922
|
+
var vis = new LO(ctx.runner.universe,
|
|
923
|
+
vRect,
|
|
924
|
+
CT.NONE, [],
|
|
925
|
+
sphere.name,
|
|
926
|
+
'closest', null, 1,
|
|
927
|
+
[240, 200, 80]);
|
|
928
|
+
vis.drawAsCircle = true;
|
|
929
|
+
// Shell render defaults (R2-3) so the Inspector's Shell section seeds live
|
|
930
|
+
// controls (render-only; no visual change — alpha 255 = opaque, shellRender 'design'
|
|
931
|
+
// = the marker default, shellShape 'circle' = the current drawAsCircle look).
|
|
932
|
+
vis.alpha = 255;
|
|
933
|
+
vis.shellRender = 'design';
|
|
934
|
+
vis.shellShape = 'circle';
|
|
935
|
+
vis._schemaKind = 'LargeObject';
|
|
936
|
+
// STUDIO Design-mode-ONLY marker — the renderer suppresses it during Run and
|
|
937
|
+
// in pure-output/standalone bundles (a render toggle at the output boundary,
|
|
938
|
+
// zero physics). AuthorView flips a Design-mode signal; see the renderer.
|
|
939
|
+
vis._designMarker = true;
|
|
940
|
+
sphere.attachMarker(vis);
|
|
941
|
+
return sphere;
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
export { SphereEmitter };
|
|
946
|
+
|