@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,760 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QuantumEmitter — Engine-agnostic emitter wrapper.
|
|
5
|
+
* Engine-agnostic: works with both the JS reference engine and the Rust/WASM engine.
|
|
6
|
+
*
|
|
7
|
+
* SUGAR: builds an emState bag and pushes the shared engine-agnostic emit-loop
|
|
8
|
+
* (emitterProgram.buildEmitterTickFn) onto universe.experimentTickFunctions, then mutates the
|
|
9
|
+
* emState directly (active/velocity/burst/source-rects). There is NO provider emitter ABI —
|
|
10
|
+
* the emit-loop is composed entirely from the generic primitives universe.random / makeLeafFrame /
|
|
11
|
+
* injectFrame, identical on both engines.
|
|
12
|
+
*
|
|
13
|
+
* Emits the CANONICAL spec: charge in raw form (0/1/'POSITIVE'/'NEGATIVE'/null).
|
|
14
|
+
* All charge translation (numeric↔string) lives in the shim providers
|
|
15
|
+
* (compat/providers/js-engine.js and compat/providers/wasm-engine.js).
|
|
16
|
+
* No engine-sniffing above the shim (§2 invariant).
|
|
17
|
+
*
|
|
18
|
+
* chargeMode / random_direction (sugar-layer modes, emitter-modes-sugar):
|
|
19
|
+
* chargeMode ∈ 'random' | 'positive' | 'negative' | 'both'
|
|
20
|
+
* random_direction ∈ boolean
|
|
21
|
+
*
|
|
22
|
+
* The sugar layer manages a SET of underlying emitter handle-programs keyed by
|
|
23
|
+
* (charge, random_direction), lazily built via buildEmitterTickFn (a host tick-fn).
|
|
24
|
+
* The chargeMode / random_direction setters toggle the active flag on the right
|
|
25
|
+
* emState handles — the engine itself is NEVER changed.
|
|
26
|
+
*
|
|
27
|
+
* 'both' = two co-located underlying emitters (POSITIVE + NEGATIVE), both active.
|
|
28
|
+
*
|
|
29
|
+
* Back-compat: the legacy `charge` constructor param maps onto chargeMode:
|
|
30
|
+
* null → 'random'
|
|
31
|
+
* 'POSITIVE' → 'positive'
|
|
32
|
+
* 'NEGATIVE' → 'negative'
|
|
33
|
+
* Existing experiments keep working unchanged.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { LargeObject } from '../../parts/LargeObject.js';
|
|
37
|
+
import * as EmitterProgram from './emitterProgram.js';
|
|
38
|
+
import { registerKind } from '../kinds.js';
|
|
39
|
+
import { CollisionTypes } from '../../parts/CollisionTypes.js';
|
|
40
|
+
import * as ValueNormalizer from '../schema/valueNormalizer.js';
|
|
41
|
+
import { declaredCount } from '../ObjectFactory.js';
|
|
42
|
+
|
|
43
|
+
var rectToArray = LargeObject.rectToArray;
|
|
44
|
+
var buildEmitterTickFn = EmitterProgram.buildEmitterTickFn;
|
|
45
|
+
|
|
46
|
+
// ── Internal helpers ─────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Map a legacy charge constructor param to a chargeMode string.
|
|
50
|
+
* @param {*} charge - null | 'POSITIVE' | 'NEGATIVE' | 0 | 1 | 'Positive' | 'Negative'
|
|
51
|
+
* @returns {'random'|'positive'|'negative'}
|
|
52
|
+
*/
|
|
53
|
+
function _chargeToMode(charge) {
|
|
54
|
+
if (charge === null || charge === undefined) return 'random';
|
|
55
|
+
if (typeof charge === 'string') {
|
|
56
|
+
var u = charge.toUpperCase();
|
|
57
|
+
if (u === 'POSITIVE' || charge === 'Positive' || charge === '+') return 'positive';
|
|
58
|
+
if (u === 'NEGATIVE' || charge === 'Negative' || charge === '-') return 'negative';
|
|
59
|
+
}
|
|
60
|
+
if (charge === 0) return 'positive';
|
|
61
|
+
if (charge === 1) return 'negative';
|
|
62
|
+
return 'random';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Map a chargeMode string to the canonical charge value expected by the emitter handle-program.
|
|
67
|
+
* 'both' is never passed directly here — callers handle it by building two emitters.
|
|
68
|
+
* @param {'random'|'positive'|'negative'} mode
|
|
69
|
+
* @returns {null|'POSITIVE'|'NEGATIVE'}
|
|
70
|
+
*/
|
|
71
|
+
function _modeToCharge(mode) {
|
|
72
|
+
if (mode === 'positive') return 'POSITIVE';
|
|
73
|
+
if (mode === 'negative') return 'NEGATIVE';
|
|
74
|
+
return null; // 'random'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build the canonical key used to look up a handle in _emitterMap.
|
|
79
|
+
* @param {null|'POSITIVE'|'NEGATIVE'} charge
|
|
80
|
+
* @param {boolean} random_direction
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function _handleKey(charge, random_direction) {
|
|
84
|
+
return String(charge) + ':' + String(!!random_direction);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── QuantumEmitter ──────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Engine-agnostic emitter wrapper with chargeMode/random_direction modes.
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} universe - The engine universe instance (JS or WASM)
|
|
93
|
+
* @param {Array<Array|Object>} source_rects - Emission regions
|
|
94
|
+
* @param {number} vx - Initial X velocity (voltage direction)
|
|
95
|
+
* @param {number} vy - Initial Y velocity
|
|
96
|
+
* @param {number} burst_size - Particles per fire (amperage)
|
|
97
|
+
* @param {*} charge - 0/1/'POSITIVE'/'NEGATIVE'/null(random) [back-compat; maps to chargeMode]
|
|
98
|
+
* @param {boolean} auto_fire - Whether to auto-fire each tick
|
|
99
|
+
* @param {boolean} random_direction - Randomize emission direction
|
|
100
|
+
* @param {number} quantum_limit - Max quanta to emit (Infinity = unlimited)
|
|
101
|
+
* @param {number} delay - Ticks between bursts
|
|
102
|
+
* @param {boolean} wrap - Wrap emitted particles at canvas boundaries.
|
|
103
|
+
* @param {Object} [opts] - the
|
|
104
|
+
* emitter source mode consumes the SAME normalized SOURCE_SPEC bag:
|
|
105
|
+
* `{ wavelength, zMin, zDepth, zIsotropy }` — the SAME trailing-`opts`
|
|
106
|
+
* idiom + the SAME field names/semantics `PointSource`'s own constructor
|
|
107
|
+
* already established for this EXACT emState contract (`emitterProgram.js`
|
|
108
|
+
* already reads `em.wavelength`/`em.zMin`/`em.zDepth`; QuantumEmitter
|
|
109
|
+
* simply never wired them). Every field OMITTED (default) — byte-identical
|
|
110
|
+
* for every existing caller (none pass a 12th arg).
|
|
111
|
+
* @param {number|null} [opts.wavelength=null] - Per-emitted-particle
|
|
112
|
+
* wavelength override (Engine Invariant #6 `mass=max(1,capacity/wavelength)`).
|
|
113
|
+
* `null` (default) = engine default (`mass=capacity`). LIVE via
|
|
114
|
+
* `setWavelength()`.
|
|
115
|
+
* @param {number|null} [opts.zMin=null] - When provided (with `opts.zDepth`),
|
|
116
|
+
* every emitted leaf's `z` is drawn uniformly from `[zMin, zMin+zDepth)`
|
|
117
|
+
* (one extra `random()` draw, opt-in only — see `emitterProgram.js`'s
|
|
118
|
+
* DRAW-ORDER CONTRACT). `null` (default) = no z ever written.
|
|
119
|
+
* @param {number|null} [opts.zDepth=null] - Depth of the uniform z-spread
|
|
120
|
+
* (paired with `opts.zMin`). Ignored if `opts.zMin` is omitted.
|
|
121
|
+
* @param {boolean} [opts.zIsotropy=false] - When BOTH this AND
|
|
122
|
+
* `random_direction` are true: the random-direction draw becomes a full
|
|
123
|
+
* 3D unit-sphere direction (azimuth + polar angle) instead of a 2D-plane
|
|
124
|
+
* circle, and the drawn `vz` is written via `setFrameProperty` — the
|
|
125
|
+
* "velocity z-symmetry consistent with its profile" half of AC1 (a
|
|
126
|
+
* DIRECTED emitter, `random_direction:false`, stays `vz=0` deliberately —
|
|
127
|
+
* collimation is unaffected regardless of this flag, matching
|
|
128
|
+
* `sustainedSource.js`'s own `'beam'` profile). Default `false` — zero
|
|
129
|
+
* extra draws for every existing `random_direction:true` caller.
|
|
130
|
+
*
|
|
131
|
+
* @deprecated V1 sugar, kept for byte-identical scenes (blackbody's OvenBox). V2 direction: SphereEmitter (needs an opt-in fixed-velocity line source that draws nothing for velocity).
|
|
132
|
+
*/
|
|
133
|
+
function QuantumEmitter(universe, source_rects, vx, vy, burst_size, charge, auto_fire, random_direction, quantum_limit, delay, wrap, opts) {
|
|
134
|
+
// Defaults
|
|
135
|
+
burst_size = (burst_size !== undefined) ? burst_size : 1;
|
|
136
|
+
if (charge === undefined) charge = null;
|
|
137
|
+
auto_fire = (auto_fire !== undefined) ? auto_fire : true;
|
|
138
|
+
random_direction = (random_direction !== undefined) ? random_direction : false;
|
|
139
|
+
quantum_limit = (quantum_limit !== undefined) ? quantum_limit : Infinity;
|
|
140
|
+
delay = (delay !== undefined) ? delay : 0;
|
|
141
|
+
wrap = (wrap !== undefined) ? wrap : false;
|
|
142
|
+
opts = opts || {};
|
|
143
|
+
var wavelength = opts.wavelength !== undefined ? opts.wavelength : null;
|
|
144
|
+
var zMin = opts.zMin !== undefined ? opts.zMin : null;
|
|
145
|
+
var zDepth = opts.zDepth !== undefined ? opts.zDepth : null;
|
|
146
|
+
var zIsotropy = !!opts.zIsotropy;
|
|
147
|
+
|
|
148
|
+
this.universe = universe;
|
|
149
|
+
this._schemaKind = 'QuantumEmitter';
|
|
150
|
+
this.vx = vx;
|
|
151
|
+
this.vy = vy;
|
|
152
|
+
this.burst_size = burst_size;
|
|
153
|
+
this.auto_fire = auto_fire;
|
|
154
|
+
// colorOverride: render-only attribute for emitted particle color.
|
|
155
|
+
// Set via updateObjectProperty(lo, 'colorOverride', [r,g,b]) from the IDE.
|
|
156
|
+
this.colorOverride = null;
|
|
157
|
+
// renderShape / renderOutline (R3-1): render-only per-emitted-frame overrides,
|
|
158
|
+
// peers of colorOverride — the same per-frame reach SphereEmitter3D has. null default →
|
|
159
|
+
// no key stamped (byte-identical). Threaded through the em spec → emitterProgram → the
|
|
160
|
+
// makeLeafFrame WASM string_map merge stamp (render.shape / render.outline).
|
|
161
|
+
this.renderShape = null;
|
|
162
|
+
this.renderOutline = null;
|
|
163
|
+
// Exposed for reads (round-trip save/restore, inspector) —
|
|
164
|
+
// mutate via setWavelength(), never a bare assignment (mirrors vx/vy's
|
|
165
|
+
// own setVelocity()-only-mutation discipline).
|
|
166
|
+
this.wavelength = wavelength;
|
|
167
|
+
|
|
168
|
+
// Direct properties exposed so serializeDesign/_captureProps can read them for
|
|
169
|
+
// round-trip save/restore. Kept in sync with _specTemplate via setBurstSize etc.
|
|
170
|
+
this.quantum_limit = (quantum_limit === Infinity) ? null : quantum_limit;
|
|
171
|
+
this.delay = delay;
|
|
172
|
+
this.wrap = wrap;
|
|
173
|
+
// source_rects: normalized copy so the schema field is readable by the inspector.
|
|
174
|
+
this.source_rects = (source_rects || []).map(function (r) { return rectToArray(r); });
|
|
175
|
+
|
|
176
|
+
// Sugar-layer mode state (backing stores for the getters/setters below).
|
|
177
|
+
this._chargeMode = _chargeToMode(charge);
|
|
178
|
+
this._random_direction = !!random_direction;
|
|
179
|
+
|
|
180
|
+
// Shared spec template used when lazily registering underlying emitters.
|
|
181
|
+
this._specTemplate = {
|
|
182
|
+
source_rects: this.source_rects,
|
|
183
|
+
vx: vx,
|
|
184
|
+
vy: vy,
|
|
185
|
+
burst_size: burst_size,
|
|
186
|
+
auto_fire: auto_fire,
|
|
187
|
+
quantum_limit: this.quantum_limit,
|
|
188
|
+
delay: delay,
|
|
189
|
+
wrap: wrap,
|
|
190
|
+
wavelength: wavelength,
|
|
191
|
+
zMin: zMin,
|
|
192
|
+
zDepth: zDepth,
|
|
193
|
+
zIsotropy: zIsotropy
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Map of handleKey → engine handle (lazily populated).
|
|
197
|
+
this._emitterMap = {};
|
|
198
|
+
|
|
199
|
+
// Register the initial set of engine emitters for the starting mode.
|
|
200
|
+
this._ensureAndActivate(this._chargeMode, this._random_direction);
|
|
201
|
+
|
|
202
|
+
// Back-compat: expose a single .handle for callers that reference it directly.
|
|
203
|
+
// For 'both', this points to the POSITIVE handle (first registered).
|
|
204
|
+
this.handle = this._primaryHandle();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Register an underlying emitter for a given charge + random_direction combo
|
|
209
|
+
* if it does not yet exist. Returns the handle.
|
|
210
|
+
* @param {null|'POSITIVE'|'NEGATIVE'} charge
|
|
211
|
+
* @param {boolean} random_direction
|
|
212
|
+
* @returns {*} handle
|
|
213
|
+
*/
|
|
214
|
+
QuantumEmitter.prototype._ensureHandle = function (charge, random_direction) {
|
|
215
|
+
var key = _handleKey(charge, random_direction);
|
|
216
|
+
if (this._emitterMap[key] !== undefined) return this._emitterMap[key];
|
|
217
|
+
|
|
218
|
+
// G-EMIT-2: build the emitter as an engine-agnostic handle-program. The sugar OWNS the
|
|
219
|
+
// emState; buildEmitterTickFn composes universe.random/makeLeafFrame/injectFrame. No
|
|
220
|
+
// provider registerEmitter — the tick-fn is pushed straight onto experimentTickFunctions.
|
|
221
|
+
var tmpl = this._specTemplate;
|
|
222
|
+
var em = {
|
|
223
|
+
sourceRects: (tmpl.source_rects || []).map(function (r) {
|
|
224
|
+
return Array.isArray(r) ? r : [r.left, r.top, r.w, r.h];
|
|
225
|
+
}),
|
|
226
|
+
vx: tmpl.vx || 0,
|
|
227
|
+
vy: tmpl.vy || 0,
|
|
228
|
+
burstSize: tmpl.burst_size || 1,
|
|
229
|
+
charge: charge, // null | 'POSITIVE' | 'NEGATIVE'
|
|
230
|
+
randomDirection: !!random_direction,
|
|
231
|
+
quantumLimit: (tmpl.quantum_limit !== undefined && tmpl.quantum_limit !== null) ? tmpl.quantum_limit : Infinity,
|
|
232
|
+
remainingQuanta: (tmpl.quantum_limit !== undefined && tmpl.quantum_limit !== null) ? tmpl.quantum_limit : Infinity,
|
|
233
|
+
delay: tmpl.delay || 0,
|
|
234
|
+
remainingDelay: tmpl.delay || 0,
|
|
235
|
+
active: (tmpl.auto_fire !== undefined) ? tmpl.auto_fire : true,
|
|
236
|
+
tombstoned: false,
|
|
237
|
+
color: this.colorOverride || null,
|
|
238
|
+
renderShape: this.renderShape || null, // R3-1 per-frame render.shape reach
|
|
239
|
+
renderOutline: (this.renderOutline != null) ? this.renderOutline : null, // R3-1 per-frame render.outline reach
|
|
240
|
+
wrap: !!tmpl.wrap,
|
|
241
|
+
wavelength: tmpl.wavelength || null,
|
|
242
|
+
fundamentalDim: null, // null → engine uses the universe's fundamental-dim as the leaf level
|
|
243
|
+
// The SAME em.zMin/em.zDepth fields PointSource already
|
|
244
|
+
// wires (emitterProgram.js's existing opt-in z-spread mechanism) +
|
|
245
|
+
// em.zIsotropy (gates the 3D unit-sphere
|
|
246
|
+
// random-direction draw). null/false (default) — byte-identical.
|
|
247
|
+
zMin: (tmpl.zMin !== undefined && tmpl.zMin !== null) ? tmpl.zMin : null,
|
|
248
|
+
zDepth: (tmpl.zDepth !== undefined && tmpl.zDepth !== null) ? tmpl.zDepth : null,
|
|
249
|
+
zIsotropy: !!tmpl.zIsotropy,
|
|
250
|
+
};
|
|
251
|
+
var tickFn = buildEmitterTickFn(this.universe, em);
|
|
252
|
+
// Back-reference so dispose() (Studio Controls Realignment S4) can find
|
|
253
|
+
// and splice OUT exactly the tick-fn(s) belonging to this emitter, without
|
|
254
|
+
// needing a separate id→fn registry. Harmless extra property on a function.
|
|
255
|
+
tickFn._emState = em;
|
|
256
|
+
this.universe.experimentTickFunctions.push(tickFn);
|
|
257
|
+
this._emitterMap[key] = em; // the emState IS the "handle" now
|
|
258
|
+
return em;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Ensure the handles required by `mode` + `random_direction` are registered,
|
|
263
|
+
* then activate them and deactivate all others.
|
|
264
|
+
* @param {'random'|'positive'|'negative'|'both'} mode
|
|
265
|
+
* @param {boolean} random_direction
|
|
266
|
+
*/
|
|
267
|
+
QuantumEmitter.prototype._ensureAndActivate = function (mode, random_direction) {
|
|
268
|
+
var rd = !!random_direction;
|
|
269
|
+
var self = this;
|
|
270
|
+
|
|
271
|
+
// Build the list of (charge, rd) combos the new mode requires.
|
|
272
|
+
var required = [];
|
|
273
|
+
if (mode === 'both') {
|
|
274
|
+
required.push({ charge: 'POSITIVE', rd: rd });
|
|
275
|
+
required.push({ charge: 'NEGATIVE', rd: rd });
|
|
276
|
+
} else {
|
|
277
|
+
required.push({ charge: _modeToCharge(mode), rd: rd });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Ensure all required handles are registered.
|
|
281
|
+
for (var i = 0; i < required.length; i++) {
|
|
282
|
+
self._ensureHandle(required[i].charge, required[i].rd);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Build a lookup of which keys should be active.
|
|
286
|
+
var activeKeys = {};
|
|
287
|
+
for (var j = 0; j < required.length; j++) {
|
|
288
|
+
activeKeys[_handleKey(required[j].charge, required[j].rd)] = true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Activate / deactivate all known emitters by mutating the sugar-owned emState directly
|
|
292
|
+
// (no provider setEmitterActive — the tick-fn closes over em.active).
|
|
293
|
+
var allKeys = Object.keys(self._emitterMap);
|
|
294
|
+
for (var k = 0; k < allKeys.length; k++) {
|
|
295
|
+
var key = allKeys[k];
|
|
296
|
+
self._emitterMap[key].active = !!activeKeys[key];
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Return the "primary" handle used for back-compat .handle access.
|
|
302
|
+
* For 'both', returns the POSITIVE handle (first registered).
|
|
303
|
+
* @returns {*}
|
|
304
|
+
*/
|
|
305
|
+
QuantumEmitter.prototype._primaryHandle = function () {
|
|
306
|
+
var rd = this._random_direction;
|
|
307
|
+
var mode = this._chargeMode;
|
|
308
|
+
var candidateKey;
|
|
309
|
+
|
|
310
|
+
if (mode === 'both' || mode === 'positive') {
|
|
311
|
+
candidateKey = _handleKey('POSITIVE', rd);
|
|
312
|
+
if (this._emitterMap[candidateKey] !== undefined) return this._emitterMap[candidateKey];
|
|
313
|
+
}
|
|
314
|
+
if (mode === 'negative') {
|
|
315
|
+
candidateKey = _handleKey('NEGATIVE', rd);
|
|
316
|
+
if (this._emitterMap[candidateKey] !== undefined) return this._emitterMap[candidateKey];
|
|
317
|
+
}
|
|
318
|
+
// 'random'
|
|
319
|
+
candidateKey = _handleKey(null, rd);
|
|
320
|
+
if (this._emitterMap[candidateKey] !== undefined) return this._emitterMap[candidateKey];
|
|
321
|
+
|
|
322
|
+
// Fallback: first registered handle.
|
|
323
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
324
|
+
return allKeys.length > 0 ? this._emitterMap[allKeys[0]] : null;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* DSE LAB (2026-07-13): iterate EVERY registered emState (ALL (charge,
|
|
329
|
+
* random_direction) groups, active or not) — mirrors `PointSource.prototype.
|
|
330
|
+
* forEachHandle` exactly (SAME need: a caller-supplied field, e.g.
|
|
331
|
+
* `em.everyNTicks`, must apply even after a LATER chargeMode switch activates
|
|
332
|
+
* a currently-inactive group). Lets a control (e.g. the Lab's firing-rate
|
|
333
|
+
* slider) work IDENTICALLY whether the active source is a `PointSource` or a
|
|
334
|
+
* `QuantumEmitter` — one shared method name, no source-type branching at the
|
|
335
|
+
* call site.
|
|
336
|
+
* @param {Function} fn - called once per registered emState.
|
|
337
|
+
*/
|
|
338
|
+
QuantumEmitter.prototype.forEachHandle = function (fn) {
|
|
339
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
340
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
341
|
+
fn(this._emitterMap[allKeys[i]]);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* DSE LAB (2026-07-13): return the flat array of emStates in currently-ACTIVE
|
|
347
|
+
* groups — mirrors `PointSource.prototype._activeHandles` exactly (SAME need:
|
|
348
|
+
* a manual "FIRE" trigger must only fire handles the user has actually
|
|
349
|
+
* activated, not every registered-but-inactive group). Lets a control (e.g.
|
|
350
|
+
* the Lab's FIRE button) work IDENTICALLY whether the active source is a
|
|
351
|
+
* `PointSource` or a `QuantumEmitter`.
|
|
352
|
+
* @returns {Array}
|
|
353
|
+
*/
|
|
354
|
+
QuantumEmitter.prototype._activeHandles = function () {
|
|
355
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
356
|
+
var result = [];
|
|
357
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
358
|
+
var em = this._emitterMap[allKeys[i]];
|
|
359
|
+
if (em.active) { result.push(em); }
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// ── chargeMode getter/setter ──────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* chargeMode getter/setter.
|
|
368
|
+
*
|
|
369
|
+
* Setter is the sugar-layer reconfigure trigger:
|
|
370
|
+
* emitter.chargeMode = 'both' → activates POSITIVE + NEGATIVE underlying emitters
|
|
371
|
+
* emitter.chargeMode = 'random' → activates the null-charge underlying emitter
|
|
372
|
+
* etc.
|
|
373
|
+
*
|
|
374
|
+
* Because updateObjectProperty routes engineVisible:false props as `lo[prop] = value`,
|
|
375
|
+
* this setter fires automatically when the IDE calls:
|
|
376
|
+
* runner.updateObjectProperty(emitter, 'chargeMode', 'both')
|
|
377
|
+
*/
|
|
378
|
+
Object.defineProperty(QuantumEmitter.prototype, 'chargeMode', {
|
|
379
|
+
get: function () { return this._chargeMode; },
|
|
380
|
+
set: function (mode) {
|
|
381
|
+
var validModes = { random: true, positive: true, negative: true, both: true };
|
|
382
|
+
if (!validModes[mode]) {
|
|
383
|
+
throw new TypeError(
|
|
384
|
+
'[QuantumEmitter] Invalid chargeMode "' + mode +
|
|
385
|
+
'". Expected: random | positive | negative | both'
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
this._chargeMode = mode;
|
|
389
|
+
this._ensureAndActivate(mode, this._random_direction);
|
|
390
|
+
this.handle = this._primaryHandle();
|
|
391
|
+
},
|
|
392
|
+
enumerable: true,
|
|
393
|
+
configurable: true
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// ── random_direction getter/setter ───────────────────────────────────
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* random_direction getter/setter.
|
|
400
|
+
*
|
|
401
|
+
* Switching random_direction mid-run ensures handles for the new
|
|
402
|
+
* (charge, random_direction) combos are registered and activated while
|
|
403
|
+
* old (charge, old_rd) handles are deactivated.
|
|
404
|
+
*/
|
|
405
|
+
Object.defineProperty(QuantumEmitter.prototype, 'random_direction', {
|
|
406
|
+
get: function () { return this._random_direction; },
|
|
407
|
+
set: function (rd) {
|
|
408
|
+
var brd = !!rd;
|
|
409
|
+
this._random_direction = brd;
|
|
410
|
+
this._ensureAndActivate(this._chargeMode, brd);
|
|
411
|
+
this.handle = this._primaryHandle();
|
|
412
|
+
},
|
|
413
|
+
enumerable: true,
|
|
414
|
+
configurable: true
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Enable/disable this emitter.
|
|
421
|
+
* Respects chargeMode — deactivating deactivates ALL underlying handles.
|
|
422
|
+
* @param {boolean} active
|
|
423
|
+
*/
|
|
424
|
+
QuantumEmitter.prototype.setActive = function (active) {
|
|
425
|
+
this.auto_fire = active;
|
|
426
|
+
var self = this;
|
|
427
|
+
if (active) {
|
|
428
|
+
// Reactivate only the emitters required by the current mode.
|
|
429
|
+
this._ensureAndActivate(this._chargeMode, this._random_direction);
|
|
430
|
+
} else {
|
|
431
|
+
// Deactivate all underlying emitters (mutate emState directly).
|
|
432
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
433
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
434
|
+
self._emitterMap[allKeys[i]].active = false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Change emission velocity (voltage change).
|
|
441
|
+
* Applied to ALL underlying engine handles.
|
|
442
|
+
* @param {number} vx
|
|
443
|
+
* @param {number} vy
|
|
444
|
+
*/
|
|
445
|
+
QuantumEmitter.prototype.setVelocity = function (vx, vy) {
|
|
446
|
+
this.vx = vx;
|
|
447
|
+
this.vy = vy;
|
|
448
|
+
this._specTemplate.vx = vx;
|
|
449
|
+
this._specTemplate.vy = vy;
|
|
450
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
451
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
452
|
+
this._emitterMap[allKeys[i]].vx = vx;
|
|
453
|
+
this._emitterMap[allKeys[i]].vy = vy;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Change burst size (amperage change).
|
|
459
|
+
* Applied to ALL underlying engine handles.
|
|
460
|
+
* @param {number} size
|
|
461
|
+
*/
|
|
462
|
+
QuantumEmitter.prototype.setBurstSize = function (size) {
|
|
463
|
+
this.burst_size = size;
|
|
464
|
+
this._specTemplate.burst_size = size;
|
|
465
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
466
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
467
|
+
this._emitterMap[allKeys[i]].burstSize = size;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* The emitter source mode consumes the SAME normalized SOURCE_SPEC bag:
|
|
473
|
+
* change per-emitted-particle
|
|
474
|
+
* wavelength (Engine Invariant #6 `mass=max(1,capacity/wavelength)`) LIVE —
|
|
475
|
+
* mirrors `setVelocity`/`setBurstSize`'s own "write every handle" discipline
|
|
476
|
+
* exactly (real state lives in `emState`, mutated ONLY by a dedicated setter
|
|
477
|
+
* — memory `reference_quantumemitter_no_generic_write_path`). A setup
|
|
478
|
+
* factory's `sourceState.wavelength` accessor (see `setupLightSourceLab.js`)
|
|
479
|
+
* wraps this so a page's wavelength slider works identically whether the
|
|
480
|
+
* live source is a `sustainedSource` (particleWalk) or this emitter.
|
|
481
|
+
* @param {number|null} wavelength
|
|
482
|
+
*/
|
|
483
|
+
QuantumEmitter.prototype.setWavelength = function (wavelength) {
|
|
484
|
+
this.wavelength = wavelength;
|
|
485
|
+
this._specTemplate.wavelength = wavelength;
|
|
486
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
487
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
488
|
+
this._emitterMap[allKeys[i]].wavelength = wavelength;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Reposition the emitter's single canonical source rect (Studio Controls
|
|
494
|
+
* Realignment S4, design §2.2/§2.5.5 — the "composite emitter move"
|
|
495
|
+
* accessor). One path: mutates `source_rects` on the wrapper (inspector/
|
|
496
|
+
* serializeDesign source of truth) AND every underlying emState's
|
|
497
|
+
* `sourceRects` (the sugar's real, engine-facing state — mirrors
|
|
498
|
+
* setVelocity/setBurstSize's own "write every handle" discipline). There is
|
|
499
|
+
* no provider ABI here (G-EMIT-2 retired `updateEmitterSourceRects` — the
|
|
500
|
+
* emitter is a host tick-fn, not an engine object); this IS the engine-facing
|
|
501
|
+
* write.
|
|
502
|
+
*
|
|
503
|
+
* Called by `FrqtlApp.updateObjectProperty` when
|
|
504
|
+
* `kind === 'QuantumEmitter'`/`prop === 'rect'` — mirrors PointSource's own
|
|
505
|
+
* `repositionToRect` dispatch convention.
|
|
506
|
+
*
|
|
507
|
+
* @param {Array} rectArr - [left, top, w, h]
|
|
508
|
+
*/
|
|
509
|
+
QuantumEmitter.prototype.setSourceRect = function (rectArr) {
|
|
510
|
+
var r = (rectArr && rectArr.slice) ? rectArr.slice() : rectArr;
|
|
511
|
+
// The marker this emitter owns follows its ONE position write (it is drawn on the source rect).
|
|
512
|
+
if (this._marker) { this._marker.setRect([r[0], r[1], r[2], r[3]]); }
|
|
513
|
+
this.source_rects = [r];
|
|
514
|
+
this._specTemplate.source_rects = this.source_rects;
|
|
515
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
516
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
517
|
+
this._emitterMap[allKeys[i]].sourceRects = [r.slice()];
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Full teardown (Studio Controls Realignment S4, design item 1 — the
|
|
523
|
+
* "delete leaves the emitter tick-fn running" defect). Stops this
|
|
524
|
+
* emitter for good:
|
|
525
|
+
* 1. Tombstones every underlying emState — `emitterProgram.js`'s
|
|
526
|
+
* `buildEmitterTickFn` checks `em.tombstoned` FIRST and returns `[]`
|
|
527
|
+
* immediately, so emission stops on the very next tick even before
|
|
528
|
+
* step 2 below completes (defense in depth: correct even if the splice
|
|
529
|
+
* somehow misses an entry).
|
|
530
|
+
* 2. Physically removes this emitter's host tick-fn(s) from
|
|
531
|
+
* `universe.experimentTickFunctions` (via the `_emState` back-reference
|
|
532
|
+
* stashed in `_ensureHandle`) so a repeatedly placed-and-deleted emitter
|
|
533
|
+
* does not accumulate dead closures in that array forever.
|
|
534
|
+
* Idempotent — a second call (double-delete / teardown race) is a safe no-op.
|
|
535
|
+
*/
|
|
536
|
+
QuantumEmitter.prototype.dispose = function () {
|
|
537
|
+
if (this._disposed) { return; }
|
|
538
|
+
this._disposed = true;
|
|
539
|
+
|
|
540
|
+
var allKeys = Object.keys(this._emitterMap);
|
|
541
|
+
var mine = [];
|
|
542
|
+
for (var i = 0; i < allKeys.length; i++) {
|
|
543
|
+
var em = this._emitterMap[allKeys[i]];
|
|
544
|
+
em.tombstoned = true;
|
|
545
|
+
em.active = false;
|
|
546
|
+
mine.push(em);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (this.universe && Array.isArray(this.universe.experimentTickFunctions)) {
|
|
550
|
+
var fns = this.universe.experimentTickFunctions;
|
|
551
|
+
for (var j = fns.length - 1; j >= 0; j--) {
|
|
552
|
+
if (fns[j]._emState && mine.indexOf(fns[j]._emState) !== -1) {
|
|
553
|
+
fns.splice(j, 1);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// The marker is this emitter's (attachMarker): it leaves the world with it.
|
|
558
|
+
if (this._marker && typeof this._marker.dispose === 'function') { this._marker.dispose(); }
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* attachMarker(lo) — take ownership of the Design marker (the NONE-collision circle LargeObject
|
|
563
|
+
* the ObjectFactory builder draws at this emitter). Called ONCE, by that builder. Stamps the ONE
|
|
564
|
+
* up-link (`lo._owner = this`, so a pick on the disc resolves to THIS emitter) and keeps the one
|
|
565
|
+
* down-link (`_marker`) this emitter moves it and disposes it through. Zero physics.
|
|
566
|
+
* @param {LargeObject} lo @returns {QuantumEmitter} this
|
|
567
|
+
*/
|
|
568
|
+
QuantumEmitter.prototype.attachMarker = function (lo) {
|
|
569
|
+
if (!lo || typeof lo !== 'object') {
|
|
570
|
+
throw new TypeError('[QuantumEmitter.attachMarker] needs the marker LargeObject. (FAIL-LOUD.)');
|
|
571
|
+
}
|
|
572
|
+
if (this._marker) {
|
|
573
|
+
throw new Error('[QuantumEmitter.attachMarker] this emitter already owns a marker. (FAIL-LOUD.)');
|
|
574
|
+
}
|
|
575
|
+
this._marker = lo;
|
|
576
|
+
lo._owner = this;
|
|
577
|
+
return this;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// ── The kind (objects/kinds.js): descriptor, construction defaults, builder ──────────
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* QuantumEmitter — Engine-registered particle emitter.
|
|
584
|
+
* Constructor: QuantumEmitter(universe, source_rects, vx, vy, burst_size,
|
|
585
|
+
* charge, auto_fire, random_direction,
|
|
586
|
+
* quantum_limit, delay, wrap)
|
|
587
|
+
*/
|
|
588
|
+
var QuantumEmitterDescriptor = {
|
|
589
|
+
description: 'A particle emitter that fires leaf quanta (particles) from one or more ' +
|
|
590
|
+
'source rectangles. Supports directed or random emission, charge polarity, ' +
|
|
591
|
+
'burst-size (amperage), and velocity (voltage direction). ' +
|
|
592
|
+
'The chargeMode/random_direction sugar layer composes multiple underlying engine ' +
|
|
593
|
+
'handles to implement "both" charge and random direction without engine changes.',
|
|
594
|
+
fields: [
|
|
595
|
+
{
|
|
596
|
+
name: 'rect',
|
|
597
|
+
type: 'rect',
|
|
598
|
+
engineVisible: true,
|
|
599
|
+
description: 'Canonical bounding rect ' + ValueNormalizer.rectTupleText(4) + ' — repositions the single emission ' +
|
|
600
|
+
'source rect. WS Studio Controls Realignment S4: routed through ' +
|
|
601
|
+
'QuantumEmitter.setSourceRect() (mutates emState.sourceRects directly — the ' +
|
|
602
|
+
'RETIRED provider ABI updateEmitterSourceRects no longer exists post-G-EMIT-2). ' +
|
|
603
|
+
'The emitter moves its own marker.'
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'source_rects',
|
|
607
|
+
type: 'rect-array',
|
|
608
|
+
engineVisible: true,
|
|
609
|
+
description: 'Emission regions as array of ' + ValueNormalizer.rectTupleText(4) + ' rects'
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: 'vx',
|
|
613
|
+
type: 'number',
|
|
614
|
+
constraint: { integer: false },
|
|
615
|
+
engineVisible: true,
|
|
616
|
+
unit: 'px/tick',
|
|
617
|
+
description: 'Initial X velocity of emitted particles (pixels/tick). Positive = rightward, negative = leftward.'
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'vy',
|
|
621
|
+
type: 'number',
|
|
622
|
+
constraint: { integer: false },
|
|
623
|
+
engineVisible: true,
|
|
624
|
+
unit: 'px/tick',
|
|
625
|
+
description: 'Initial Y velocity of emitted particles (pixels/tick). Positive = downward (screen coords), negative = upward.'
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: 'burst_size',
|
|
629
|
+
type: 'number',
|
|
630
|
+
constraint: { min: 1, integer: true },
|
|
631
|
+
engineVisible: true,
|
|
632
|
+
description: 'Number of particles fired per burst (amperage). Higher values produce more particles per tick; lower values reduce flux.'
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'auto_fire',
|
|
636
|
+
type: 'boolean',
|
|
637
|
+
engineVisible: true,
|
|
638
|
+
description: 'Whether the emitter fires automatically every tick. false = must be triggered manually (e.g. by a tick function or the inspector).'
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'quantum_limit',
|
|
642
|
+
type: 'number',
|
|
643
|
+
constraint: { min: 1, integer: true },
|
|
644
|
+
engineVisible: true,
|
|
645
|
+
description: 'Maximum total quanta to emit (null/Infinity = unlimited). The emitter auto-deactivates after reaching this count.'
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: 'delay',
|
|
649
|
+
type: 'number',
|
|
650
|
+
constraint: { min: 0, integer: true },
|
|
651
|
+
engineVisible: true,
|
|
652
|
+
unit: 'ticks',
|
|
653
|
+
description: 'Ticks between bursts. 0 = fire every tick; N = fire once every N+1 ticks.'
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: 'wrap',
|
|
657
|
+
type: 'boolean',
|
|
658
|
+
engineVisible: true,
|
|
659
|
+
description: 'Wrap emitted particles at canvas boundaries instead of letting them escape.'
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: 'colorOverride',
|
|
663
|
+
type: 'rgb',
|
|
664
|
+
engineVisible: false,
|
|
665
|
+
description: 'Override render color for emitted particles [r, g, b] (render-only; stored on the emitter wrapper)'
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: 'chargeMode',
|
|
669
|
+
type: 'enum',
|
|
670
|
+
constraint: { values: { random: 'random', positive: 'positive', negative: 'negative', both: 'both' } },
|
|
671
|
+
engineVisible: false,
|
|
672
|
+
description: 'Charge mode (user-facing sugar): random = null charge, positive, negative, or both (two co-located emitters). Composes underlying emitter handle-programs by toggling their active flag — no engine ABI change. Use this instead of the raw charge param.'
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: 'random_direction',
|
|
676
|
+
type: 'boolean',
|
|
677
|
+
engineVisible: false,
|
|
678
|
+
description: 'Randomize emission direction each burst (sugar layer). Registers a new underlying handle with rd=true — no engine ABI change.'
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: 'layer',
|
|
682
|
+
type: 'number',
|
|
683
|
+
constraint: { min: 0, integer: true },
|
|
684
|
+
engineVisible: false,
|
|
685
|
+
description: 'Dimension layer this emitter targets (0 = fundamental; null = default). Emitted particles are placed in this layer. Stored on the emitter wrapper; no engine ABI change required.'
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
registerKind(QuantumEmitter, {
|
|
691
|
+
from: import.meta.url,
|
|
692
|
+
descriptor: QuantumEmitterDescriptor,
|
|
693
|
+
defaults: {
|
|
694
|
+
source_rects: [[50, 95, 10, 10]],
|
|
695
|
+
vx: 1, vy: 0,
|
|
696
|
+
burst_size: 1,
|
|
697
|
+
chargeMode: 'random',
|
|
698
|
+
auto_fire: true,
|
|
699
|
+
random_direction: false,
|
|
700
|
+
quantum_limit: null,
|
|
701
|
+
delay: 0,
|
|
702
|
+
wrap: false
|
|
703
|
+
},
|
|
704
|
+
build: function (props, ctx, Class) {
|
|
705
|
+
var QE = Class; // the class that registered (a composed fork's own class)
|
|
706
|
+
var LO2 = LargeObject;
|
|
707
|
+
var CT2 = CollisionTypes;
|
|
708
|
+
var ew = 20, eh = 20;
|
|
709
|
+
var er;
|
|
710
|
+
if (props.rect) {
|
|
711
|
+
er = [props.rect.left || 0, props.rect.top || 0,
|
|
712
|
+
props.rect.w || ew, props.rect.h || eh];
|
|
713
|
+
} else if (props.source_rects && props.source_rects.length) {
|
|
714
|
+
var sr0 = props.source_rects[0];
|
|
715
|
+
er = sr0.slice ? sr0.slice() : sr0;
|
|
716
|
+
} else {
|
|
717
|
+
er = [Math.round(ctx.x - ew / 2), Math.round(ctx.y - eh / 2), ew, eh];
|
|
718
|
+
}
|
|
719
|
+
// Bug #1 fix: pass null as raw charge; set chargeMode via the sugar setter AFTER
|
|
720
|
+
// construction so the correct underlying engine handles are registered.
|
|
721
|
+
// The old props.charge path was broken: the schema only captures chargeMode now.
|
|
722
|
+
var qemitter = new QE(
|
|
723
|
+
ctx.runner.universe,
|
|
724
|
+
[er.slice ? er.slice() : er],
|
|
725
|
+
props.vx != null ? props.vx : 1,
|
|
726
|
+
props.vy != null ? props.vy : 0,
|
|
727
|
+
props.burst_size != null ? props.burst_size : 1,
|
|
728
|
+
null, // raw charge — overridden immediately below via chargeMode setter
|
|
729
|
+
props.auto_fire != null ? props.auto_fire : true,
|
|
730
|
+
props.random_direction != null ? props.random_direction : false,
|
|
731
|
+
props.quantum_limit != null ? props.quantum_limit : Infinity,
|
|
732
|
+
props.delay != null ? props.delay : 0,
|
|
733
|
+
props.wrap || false
|
|
734
|
+
);
|
|
735
|
+
qemitter._schemaKind = Class.name;
|
|
736
|
+
// Restore chargeMode via the sugar setter (handles 'random'|'positive'|'negative'|'both').
|
|
737
|
+
if (props.chargeMode) { qemitter.chargeMode = props.chargeMode; }
|
|
738
|
+
if (props.colorOverride) { qemitter.colorOverride = props.colorOverride; }
|
|
739
|
+
|
|
740
|
+
// Visual handle (circle LargeObject) so hand tool can drag it.
|
|
741
|
+
// layer → observedDimensions: the visual handle tracks the emitter's layer.
|
|
742
|
+
var qvisObsDims = (props.layer != null) ? [props.layer] : null;
|
|
743
|
+
var qName = props.name || ('emitter ' + declaredCount(ctx.runner, 'QuantumEmitter'));
|
|
744
|
+
qemitter.name = qName; // the declared name is the emitter's; its marker wears it
|
|
745
|
+
var qvis = new LO2(ctx.runner.universe,
|
|
746
|
+
[er[0], er[1], er[2] || ew, er[3] || eh],
|
|
747
|
+
CT2.NONE, [],
|
|
748
|
+
qName,
|
|
749
|
+
'closest', qvisObsDims, 1, props.color || [80, 200, 80]);
|
|
750
|
+
qvis.drawAsCircle = true;
|
|
751
|
+
qvis._schemaKind = 'LargeObject';
|
|
752
|
+
qemitter.attachMarker(qvis);
|
|
753
|
+
return qemitter;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// ── Export ───────────────────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
export { QuantumEmitter };
|
|
760
|
+
|