@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,762 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PointSource — Engine-agnostic radial emitter.
|
|
5
|
+
* Creates one emitter per emission angle to preserve the deterministic
|
|
6
|
+
* angle pattern. Each angle gets its own emitter with fixed velocity
|
|
7
|
+
* vx=radius*cos(a), vy=radius*sin(a).
|
|
8
|
+
*
|
|
9
|
+
* Engine-agnostic: works with both the JS reference engine and the Rust/WASM engine.
|
|
10
|
+
* Emits raw canonical charge; providers translate to engine wire-format.
|
|
11
|
+
* No engine-sniffing above the shim (§2 invariant).
|
|
12
|
+
*
|
|
13
|
+
* chargeMode / random_direction (sugar-layer modes, emitter-modes-sugar):
|
|
14
|
+
* chargeMode ∈ 'random' | 'positive' | 'negative' | 'both'
|
|
15
|
+
* random_direction ∈ boolean
|
|
16
|
+
*
|
|
17
|
+
* For PointSource, chargeMode operates on GROUPS of per-angle handles.
|
|
18
|
+
* Each (charge, random_direction) combo has its own full set of N per-angle handles.
|
|
19
|
+
* Switching chargeMode registers the needed group(s) lazily and activates/deactivates.
|
|
20
|
+
*
|
|
21
|
+
* 'both' = two per-angle handle groups (POSITIVE + NEGATIVE), both active → 2N handles.
|
|
22
|
+
*
|
|
23
|
+
* Back-compat: legacy `charge` constructor param maps to chargeMode:
|
|
24
|
+
* null → 'random'
|
|
25
|
+
* 'POSITIVE' → 'positive'
|
|
26
|
+
* 'NEGATIVE' → 'negative'
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import * as EmitterProgram from '../objects/objects/emitterProgram.js';
|
|
30
|
+
import { registerKind } from '../objects/kinds.js';
|
|
31
|
+
import { LargeObject } from './LargeObject.js';
|
|
32
|
+
import { CollisionTypes } from './CollisionTypes.js';
|
|
33
|
+
import * as ValueNormalizer from '../objects/schema/valueNormalizer.js';
|
|
34
|
+
import { declaredCount } from '../objects/ObjectFactory.js';
|
|
35
|
+
|
|
36
|
+
// G-EMIT-2: the emitter is engine-agnostic sugar (emitterProgram.js).
|
|
37
|
+
var buildEmitterTickFn;
|
|
38
|
+
buildEmitterTickFn = EmitterProgram.buildEmitterTickFn;
|
|
39
|
+
|
|
40
|
+
// ── Internal helpers ─────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Map a legacy charge constructor param to a chargeMode string.
|
|
44
|
+
*/
|
|
45
|
+
function _chargeToMode(charge) {
|
|
46
|
+
if (charge === null || charge === undefined) return 'random';
|
|
47
|
+
if (typeof charge === 'string') {
|
|
48
|
+
var u = charge.toUpperCase();
|
|
49
|
+
if (u === 'POSITIVE' || charge === 'Positive' || charge === '+') return 'positive';
|
|
50
|
+
if (u === 'NEGATIVE' || charge === 'Negative' || charge === '-') return 'negative';
|
|
51
|
+
}
|
|
52
|
+
if (charge === 0) return 'positive';
|
|
53
|
+
if (charge === 1) return 'negative';
|
|
54
|
+
return 'random';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Map chargeMode string to canonical charge value for the emitter handle-program.
|
|
59
|
+
* 'both' callers build POSITIVE and NEGATIVE emitters separately.
|
|
60
|
+
*/
|
|
61
|
+
function _modeToCharge(mode) {
|
|
62
|
+
if (mode === 'positive') return 'POSITIVE';
|
|
63
|
+
if (mode === 'negative') return 'NEGATIVE';
|
|
64
|
+
return null; // 'random'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Canonical key for a handle group: identifies the (charge, random_direction) combo.
|
|
69
|
+
*/
|
|
70
|
+
function _groupKey(charge, random_direction) {
|
|
71
|
+
return String(charge) + ':' + String(!!random_direction);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build the list of emission radii (max → min in steps of 2).
|
|
76
|
+
*/
|
|
77
|
+
function _buildRadii(radius, minRadius) {
|
|
78
|
+
var radii = [];
|
|
79
|
+
if (minRadius !== null && minRadius !== undefined) {
|
|
80
|
+
var maxR = Math.max(minRadius, radius);
|
|
81
|
+
for (var r = maxR; r >= minRadius; r -= 2) {
|
|
82
|
+
radii.push(r);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
radii.push(radius);
|
|
86
|
+
}
|
|
87
|
+
return radii;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── PointSource ─────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Radial emitter that fires particles outward from the surface of a circle.
|
|
94
|
+
*
|
|
95
|
+
* It registers one fixed-direction emitter per emission angle (so the radial
|
|
96
|
+
* pattern is deterministic), each firing 1px/tick leaf Frames. There is no
|
|
97
|
+
* wave equation — interference is emergent from where these particle streams
|
|
98
|
+
* overlap.
|
|
99
|
+
*
|
|
100
|
+
* ## Physics you can drive (and what changes on screen)
|
|
101
|
+
* - **`opts.wavelength`** sets each emitted particle's **mass**
|
|
102
|
+
* (`mass = capacity / wavelength`). Mass feeds the per-Frame oscillation that
|
|
103
|
+
* produces wave behavior. In the wave-interference setup `wavelength = 32`
|
|
104
|
+
* yields leaf mass ≈ 1 — the clean-fringe regime that also matches the WASM
|
|
105
|
+
* engine. This is DISTINCT from the universe `wavelengthScale` (below).
|
|
106
|
+
* - **Fringe spacing** is governed by the universe `wavelengthScale`
|
|
107
|
+
* (`EGPTfrqtl` / `universe.init(...)`), NOT by this constructor:
|
|
108
|
+
* `wavelength = floor((capacity / mass) × WAVELENGTH_CONSTANT)`. OBSERVED
|
|
109
|
+
* (seed 42): raising it 2 → 32 widens the fringes from a near-uniform dense
|
|
110
|
+
* cloud to sparse, widely-separated radial spokes.
|
|
111
|
+
* - **`angle_increment`** sets the number of emission rays = `floor(360 / inc)`
|
|
112
|
+
* (5° → 72 rays). Smaller increment = denser angular coverage = a smoother
|
|
113
|
+
* radial field; larger = visible discrete spokes.
|
|
114
|
+
* - **`charge`** (`null` = random ±) drives the oscillation quadrant per
|
|
115
|
+
* particle; with mixed charge the two streams produce the canonical fringes.
|
|
116
|
+
* - **`quantum_limit`** caps how many quanta each angle emits before stopping
|
|
117
|
+
* (the steady-state density ceiling); `Infinity` = unlimited.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} universe - The engine universe instance (JS or WASM).
|
|
120
|
+
* @param {number} x - Center X of the point source (physics pixels).
|
|
121
|
+
* @param {number} y - Center Y of the point source (physics pixels).
|
|
122
|
+
* @param {number} diameter - Diameter of the source circle. Emission radius =
|
|
123
|
+
* `diameter / 2`; this radius ALSO sets the emission velocity (see `speed`).
|
|
124
|
+
* @param {number} speed - CURRENTLY UNUSED. It is stored as `this.speed` but
|
|
125
|
+
* emission velocity is derived from the source radius (`diameter / 2`), not
|
|
126
|
+
* this value (`_ensureGroup`: `vx = emitR·cos θ`, `vy = emitR·sin θ`). The
|
|
127
|
+
* wave-interference setup passes `diameter/2` here, which coincidentally
|
|
128
|
+
* equals the radius, masking that it is ignored. Flagged for review — do not
|
|
129
|
+
* rely on this parameter to change speed.
|
|
130
|
+
* @param {*} charge - `0`/`1`/`'POSITIVE'`/`'NEGATIVE'`/`null`(random per
|
|
131
|
+
* particle). Back-compat; maps to `chargeMode`.
|
|
132
|
+
* @param {boolean} [auto_fire=true] - Whether the emitters fire automatically.
|
|
133
|
+
* @param {number} [quantum_limit=Infinity] - Max quanta per angle.
|
|
134
|
+
* @param {number} [delay=0] - Ticks between bursts.
|
|
135
|
+
* @param {number} [angle_increment=5] - Degrees between emission angles →
|
|
136
|
+
* `floor(360/angle_increment)` rays.
|
|
137
|
+
* @param {Object} [opts] - `{ wavelength, color, minRadius, emergentPhysics, zMin, zDepth }`.
|
|
138
|
+
* @param {number} [opts.wavelength] - Per-emitter wavelength → particle mass
|
|
139
|
+
* (see Physics, above). Omitted → engine default leaf mass.
|
|
140
|
+
* @param {number} [opts.zMin] - when provided (with `opts.zDepth`), every
|
|
141
|
+
* emitted leaf's z is drawn UNIFORMLY from `[zMin, zMin+zDepth)` (one extra `random()` draw
|
|
142
|
+
* per leaf, opt-in only — see `emitterProgram.js`'s DRAW-ORDER CONTRACT). Omitted (default)
|
|
143
|
+
* → no z is ever written, byte-identical to pre-3D behavior. The caller (e.g.
|
|
144
|
+
* `setupWaveInterference`'s `depth` option) decides centering — PointSource just forwards.
|
|
145
|
+
* @param {number} [opts.zDepth] - Depth of the uniform z-spread (paired with `opts.zMin`).
|
|
146
|
+
* Ignored if `opts.zMin` is omitted.
|
|
147
|
+
*
|
|
148
|
+
* @deprecated V1 sugar, kept for byte-identical scenes (big-bang). V2 direction: SphereEmitter{ preset:'drum' | 'spherical' } (needs an opt-in deterministic zero-momentum ring mode to reproduce this emission).
|
|
149
|
+
*/
|
|
150
|
+
function PointSource(universe, x, y, diameter, speed, charge, auto_fire, quantum_limit, delay, angle_increment, opts) {
|
|
151
|
+
// Defaults
|
|
152
|
+
if (charge === undefined) charge = null;
|
|
153
|
+
auto_fire = (auto_fire !== undefined) ? auto_fire : true;
|
|
154
|
+
quantum_limit = (quantum_limit !== undefined) ? quantum_limit : Infinity;
|
|
155
|
+
delay = (delay !== undefined) ? delay : 0;
|
|
156
|
+
angle_increment = (angle_increment !== undefined) ? angle_increment : 5;
|
|
157
|
+
opts = opts || {};
|
|
158
|
+
|
|
159
|
+
// When emergentPhysics=true (e.g. Big Bang), particles get no initial velocity
|
|
160
|
+
var emergentPhysics = opts.emergentPhysics || false;
|
|
161
|
+
// minRadius: emit at multiple radii from maxRadius down to minRadius (step 2)
|
|
162
|
+
var minRadius = opts.minRadius !== undefined ? opts.minRadius : null;
|
|
163
|
+
// Optional color override for emitted particles (e.g. [0, 255, 0] for green)
|
|
164
|
+
var color = opts.color || null;
|
|
165
|
+
// Optional wavelength for mass computation (affects wave behavior at dim > 0)
|
|
166
|
+
var wavelength = opts.wavelength || null;
|
|
167
|
+
// Optional uniform z-spread (opt-in; null = no z ever written).
|
|
168
|
+
var zMin = opts.zMin !== undefined ? opts.zMin : null;
|
|
169
|
+
var zDepth = opts.zDepth !== undefined ? opts.zDepth : null;
|
|
170
|
+
|
|
171
|
+
this.universe = universe;
|
|
172
|
+
this._schemaKind = 'PointSource';
|
|
173
|
+
this.x = x;
|
|
174
|
+
this.y = y;
|
|
175
|
+
this.diameter = diameter;
|
|
176
|
+
this.speed = speed;
|
|
177
|
+
this.angle_increment = angle_increment;
|
|
178
|
+
this.auto_fire = auto_fire;
|
|
179
|
+
// colorOverride: render-only attribute for emitted particle color.
|
|
180
|
+
// Set via updateObjectProperty(lo, 'colorOverride', [r,g,b]) from the IDE.
|
|
181
|
+
this.colorOverride = color || null;
|
|
182
|
+
// renderShape / renderOutline (R3-1): render-only per-emitted-frame overrides,
|
|
183
|
+
// peers of colorOverride (same per-frame reach SphereEmitter3D has). null → no key stamped.
|
|
184
|
+
this.renderShape = null;
|
|
185
|
+
this.renderOutline = null;
|
|
186
|
+
|
|
187
|
+
// Sugar-layer mode state.
|
|
188
|
+
this._chargeMode = _chargeToMode(charge);
|
|
189
|
+
this._random_direction = false; // PointSource default: directed emission
|
|
190
|
+
|
|
191
|
+
// Store construction params needed for lazy handle group registration.
|
|
192
|
+
this._emergentPhysics = emergentPhysics;
|
|
193
|
+
this._color = color;
|
|
194
|
+
this._wavelength = wavelength;
|
|
195
|
+
this._quantum_limit = quantum_limit;
|
|
196
|
+
this._delay = delay;
|
|
197
|
+
this._zMin = zMin;
|
|
198
|
+
this._zDepth = zDepth;
|
|
199
|
+
|
|
200
|
+
// Pre-compute the list of emission radii (stored for lazy re-use by _ensureGroup).
|
|
201
|
+
this._radii = _buildRadii(diameter / 2, minRadius);
|
|
202
|
+
|
|
203
|
+
// Map of groupKey → Array of per-angle handles (one per (radius, angle) pair).
|
|
204
|
+
// Each group is a full complement of N×M handles (N angles × M radii).
|
|
205
|
+
this._handleGroups = {};
|
|
206
|
+
|
|
207
|
+
// Register the initial handle group for the starting mode.
|
|
208
|
+
this._ensureAndActivate(this._chargeMode, this._random_direction);
|
|
209
|
+
|
|
210
|
+
// Back-compat: emitterHandles points to the active group's handles.
|
|
211
|
+
this.emitterHandles = this._activeHandles();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Register all per-angle handles for a given (charge, random_direction) group
|
|
216
|
+
* if that group does not yet exist. Returns the handles array.
|
|
217
|
+
* @param {null|'POSITIVE'|'NEGATIVE'} charge
|
|
218
|
+
* @param {boolean} random_direction
|
|
219
|
+
* @returns {Array} handles
|
|
220
|
+
*/
|
|
221
|
+
PointSource.prototype._ensureGroup = function (charge, random_direction) {
|
|
222
|
+
var key = _groupKey(charge, random_direction);
|
|
223
|
+
if (this._handleGroups[key] !== undefined) return this._handleGroups[key];
|
|
224
|
+
|
|
225
|
+
var handles = [];
|
|
226
|
+
var universe = this.universe;
|
|
227
|
+
var x = this.x;
|
|
228
|
+
var y = this.y;
|
|
229
|
+
var angle_increment = this.angle_increment;
|
|
230
|
+
var numAngles = Math.floor(360 / angle_increment);
|
|
231
|
+
var emergentPhysics = this._emergentPhysics;
|
|
232
|
+
var color = this._color;
|
|
233
|
+
var renderShape = this.renderShape; // R3-1 per-frame render.shape reach
|
|
234
|
+
var renderOutline = this.renderOutline; // R3-1 per-frame render.outline reach
|
|
235
|
+
var wavelength = this._wavelength;
|
|
236
|
+
var quantum_limit = this._quantum_limit;
|
|
237
|
+
var delay = this._delay;
|
|
238
|
+
var radii = this._radii;
|
|
239
|
+
var rd = !!random_direction;
|
|
240
|
+
var auto_fire = this.auto_fire;
|
|
241
|
+
var zMin = this._zMin;
|
|
242
|
+
var zDepth = this._zDepth;
|
|
243
|
+
|
|
244
|
+
for (var ri = 0; ri < radii.length; ri++) {
|
|
245
|
+
var emitR = radii[ri];
|
|
246
|
+
for (var i = 0; i < numAngles; i++) {
|
|
247
|
+
var angleDeg = i * angle_increment;
|
|
248
|
+
var angleRad = angleDeg * Math.PI / 180;
|
|
249
|
+
|
|
250
|
+
var emitX = x + emitR * Math.cos(angleRad);
|
|
251
|
+
var emitY = y + emitR * Math.sin(angleRad);
|
|
252
|
+
|
|
253
|
+
var vx, vy;
|
|
254
|
+
if (emergentPhysics) {
|
|
255
|
+
vx = 0;
|
|
256
|
+
vy = 0;
|
|
257
|
+
} else {
|
|
258
|
+
vx = emitR * Math.cos(angleRad);
|
|
259
|
+
vy = emitR * Math.sin(angleRad);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var sourceRect = [Math.round(emitX), Math.round(emitY), 1, 1];
|
|
263
|
+
|
|
264
|
+
// G-EMIT-2: build a per-angle emState + push the shared emitter handle-program;
|
|
265
|
+
// the sugar OWNS the emState (the emState IS the "handle"). No registerEmitter.
|
|
266
|
+
var em = {
|
|
267
|
+
sourceRects: [sourceRect],
|
|
268
|
+
vx: vx,
|
|
269
|
+
vy: vy,
|
|
270
|
+
burstSize: 1,
|
|
271
|
+
charge: charge, // null | 'POSITIVE' | 'NEGATIVE'
|
|
272
|
+
randomDirection: rd,
|
|
273
|
+
quantumLimit: (quantum_limit === Infinity || quantum_limit == null) ? Infinity : quantum_limit,
|
|
274
|
+
remainingQuanta: (quantum_limit === Infinity || quantum_limit == null) ? Infinity : quantum_limit,
|
|
275
|
+
delay: delay || 0,
|
|
276
|
+
remainingDelay: delay || 0,
|
|
277
|
+
active: auto_fire,
|
|
278
|
+
tombstoned: false,
|
|
279
|
+
color: color || null,
|
|
280
|
+
renderShape: renderShape || null, // R3-1 per-frame render.shape reach
|
|
281
|
+
renderOutline: (renderOutline != null) ? renderOutline : null, // R3-1 render.outline reach
|
|
282
|
+
wrap: false,
|
|
283
|
+
wavelength: wavelength || null,
|
|
284
|
+
fundamentalDim: null, // null → engine uses the universe's fundamental-dim as the leaf level
|
|
285
|
+
zMin: zMin, // null (default) → no z draw/write at all
|
|
286
|
+
zDepth: zDepth,
|
|
287
|
+
};
|
|
288
|
+
var tickFn = buildEmitterTickFn(universe, em);
|
|
289
|
+
// Back-reference so dispose() (Studio Controls Realignment S4) can
|
|
290
|
+
// find and splice OUT exactly the tick-fn(s) belonging to this
|
|
291
|
+
// PointSource, mirroring QuantumEmitter's own `_emState` convention.
|
|
292
|
+
tickFn._emState = em;
|
|
293
|
+
universe.experimentTickFunctions.push(tickFn);
|
|
294
|
+
handles.push(em);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this._handleGroups[key] = handles;
|
|
299
|
+
return handles;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Ensure the handle groups required by `mode` + `random_direction` are registered,
|
|
304
|
+
* then activate them and deactivate all others.
|
|
305
|
+
* @param {'random'|'positive'|'negative'|'both'} mode
|
|
306
|
+
* @param {boolean} random_direction
|
|
307
|
+
*/
|
|
308
|
+
PointSource.prototype._ensureAndActivate = function (mode, random_direction) {
|
|
309
|
+
var rd = !!random_direction;
|
|
310
|
+
var self = this;
|
|
311
|
+
|
|
312
|
+
// Determine which (charge, rd) combos the new mode requires.
|
|
313
|
+
var required = [];
|
|
314
|
+
if (mode === 'both') {
|
|
315
|
+
required.push({ charge: 'POSITIVE', rd: rd });
|
|
316
|
+
required.push({ charge: 'NEGATIVE', rd: rd });
|
|
317
|
+
} else {
|
|
318
|
+
required.push({ charge: _modeToCharge(mode), rd: rd });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Ensure all required groups are registered.
|
|
322
|
+
for (var i = 0; i < required.length; i++) {
|
|
323
|
+
self._ensureGroup(required[i].charge, required[i].rd);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Build a lookup of which group keys should be active.
|
|
327
|
+
var activeKeys = {};
|
|
328
|
+
for (var j = 0; j < required.length; j++) {
|
|
329
|
+
activeKeys[_groupKey(required[j].charge, required[j].rd)] = true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Activate / deactivate handles in all registered groups.
|
|
333
|
+
var allGroupKeys = Object.keys(self._handleGroups);
|
|
334
|
+
for (var k = 0; k < allGroupKeys.length; k++) {
|
|
335
|
+
var key = allGroupKeys[k];
|
|
336
|
+
var groupHandles = self._handleGroups[key];
|
|
337
|
+
var shouldBeActive = !!activeKeys[key];
|
|
338
|
+
for (var h = 0; h < groupHandles.length; h++) {
|
|
339
|
+
groupHandles[h].active = shouldBeActive; // mutate sugar-owned emState
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Return the flat array of handles for all currently active groups.
|
|
346
|
+
* Used to keep this.emitterHandles in sync (back-compat for repositionToRect).
|
|
347
|
+
* @returns {Array}
|
|
348
|
+
*/
|
|
349
|
+
PointSource.prototype._activeHandles = function () {
|
|
350
|
+
var mode = this._chargeMode;
|
|
351
|
+
var rd = this._random_direction;
|
|
352
|
+
var result = [];
|
|
353
|
+
|
|
354
|
+
var activeKeys = {};
|
|
355
|
+
if (mode === 'both') {
|
|
356
|
+
activeKeys[_groupKey('POSITIVE', rd)] = true;
|
|
357
|
+
activeKeys[_groupKey('NEGATIVE', rd)] = true;
|
|
358
|
+
} else {
|
|
359
|
+
activeKeys[_groupKey(_modeToCharge(mode), rd)] = true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
var allGroupKeys = Object.keys(this._handleGroups);
|
|
363
|
+
for (var i = 0; i < allGroupKeys.length; i++) {
|
|
364
|
+
if (activeKeys[allGroupKeys[i]]) {
|
|
365
|
+
var groupHandles = this._handleGroups[allGroupKeys[i]];
|
|
366
|
+
for (var j = 0; j < groupHandles.length; j++) {
|
|
367
|
+
result.push(groupHandles[j]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* DSE LAB (2026-07-13): iterate EVERY registered emState (ALL (charge,
|
|
376
|
+
* random_direction) groups — active or not, so a field set here still applies
|
|
377
|
+
* after a LATER `chargeMode` switch activates a currently-inactive group).
|
|
378
|
+
* The clean, public way to mutate a per-emission field (e.g. `em.everyNTicks`,
|
|
379
|
+
* the firing-rate cadence) on every handle this PointSource owns, without a
|
|
380
|
+
* caller reaching into the private `_handleGroups` map directly.
|
|
381
|
+
* @param {function(Object):void} fn - called once per emState.
|
|
382
|
+
*/
|
|
383
|
+
PointSource.prototype.forEachHandle = function (fn) {
|
|
384
|
+
var allGroupKeys = Object.keys(this._handleGroups);
|
|
385
|
+
for (var i = 0; i < allGroupKeys.length; i++) {
|
|
386
|
+
var groupHandles = this._handleGroups[allGroupKeys[i]];
|
|
387
|
+
for (var j = 0; j < groupHandles.length; j++) {
|
|
388
|
+
fn(groupHandles[j]);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// ── chargeMode getter/setter ──────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* chargeMode getter/setter (sugar-layer, engine-not-touched).
|
|
397
|
+
*
|
|
398
|
+
* Because updateObjectProperty routes engineVisible:false props as `lo[prop] = value`,
|
|
399
|
+
* this setter fires automatically when the IDE calls:
|
|
400
|
+
* runner.updateObjectProperty(pointSource, 'chargeMode', 'both')
|
|
401
|
+
*/
|
|
402
|
+
Object.defineProperty(PointSource.prototype, 'chargeMode', {
|
|
403
|
+
get: function () { return this._chargeMode; },
|
|
404
|
+
set: function (mode) {
|
|
405
|
+
var validModes = { random: true, positive: true, negative: true, both: true };
|
|
406
|
+
if (!validModes[mode]) {
|
|
407
|
+
throw new TypeError(
|
|
408
|
+
'[PointSource] Invalid chargeMode "' + mode +
|
|
409
|
+
'". Expected: random | positive | negative | both'
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
this._chargeMode = mode;
|
|
413
|
+
this._ensureAndActivate(mode, this._random_direction);
|
|
414
|
+
this.emitterHandles = this._activeHandles();
|
|
415
|
+
},
|
|
416
|
+
enumerable: true,
|
|
417
|
+
configurable: true
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ── random_direction getter/setter ───────────────────────────────────
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* random_direction getter/setter.
|
|
424
|
+
*
|
|
425
|
+
* Note: PointSource normally uses directed emission (false). Enabling
|
|
426
|
+
* random_direction registers a new group of angle emitters with rd=true.
|
|
427
|
+
*/
|
|
428
|
+
Object.defineProperty(PointSource.prototype, 'random_direction', {
|
|
429
|
+
get: function () { return this._random_direction; },
|
|
430
|
+
set: function (rd) {
|
|
431
|
+
var brd = !!rd;
|
|
432
|
+
this._random_direction = brd;
|
|
433
|
+
this._ensureAndActivate(this._chargeMode, brd);
|
|
434
|
+
this.emitterHandles = this._activeHandles();
|
|
435
|
+
},
|
|
436
|
+
enumerable: true,
|
|
437
|
+
configurable: true
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Enable/disable all angle emitters.
|
|
444
|
+
* @param {boolean} active
|
|
445
|
+
*/
|
|
446
|
+
PointSource.prototype.setActive = function (active) {
|
|
447
|
+
this.auto_fire = active;
|
|
448
|
+
var self = this;
|
|
449
|
+
if (active) {
|
|
450
|
+
this._ensureAndActivate(this._chargeMode, this._random_direction);
|
|
451
|
+
} else {
|
|
452
|
+
// Deactivate all handles in all groups.
|
|
453
|
+
var allGroupKeys = Object.keys(self._handleGroups);
|
|
454
|
+
for (var i = 0; i < allGroupKeys.length; i++) {
|
|
455
|
+
var groupHandles = self._handleGroups[allGroupKeys[i]];
|
|
456
|
+
for (var j = 0; j < groupHandles.length; j++) {
|
|
457
|
+
groupHandles[j].active = false; // mutate sugar-owned emState
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Reposition the PointSource to a new [l, t, w, h] rect.
|
|
465
|
+
*
|
|
466
|
+
* This is the canonical ONE-PATH reposition for PointSource:
|
|
467
|
+
* 1. Compute new center (cx = l + w/2, cy = t + h/2).
|
|
468
|
+
* 2. Update this.x / this.y (kept in sync so schema inspector reads the live values).
|
|
469
|
+
* 3. Recompute the 1×1 source rect for each handle in ALL registered groups.
|
|
470
|
+
* 4. Call universe.updateEmitterSourceRects for each handle (engine-concurrence path).
|
|
471
|
+
*
|
|
472
|
+
* Called by FrqtlApp.updateObjectProperty when kind='PointSource', prop='rect'.
|
|
473
|
+
* Routes through universe.updateEmitterSourceRects — no second path, no engine-sniffing.
|
|
474
|
+
*
|
|
475
|
+
* For 'both' mode: both the POSITIVE and NEGATIVE groups are repositioned.
|
|
476
|
+
* The N angle emitters per group are recomputed identically (co-located).
|
|
477
|
+
*
|
|
478
|
+
* @param {number} l - New left edge (physics pixels)
|
|
479
|
+
* @param {number} t - New top edge (physics pixels)
|
|
480
|
+
* @param {number} w - New bounding width (physics pixels)
|
|
481
|
+
* @param {number} h - New bounding height (physics pixels)
|
|
482
|
+
* @param {Object} [universe] - Engine universe (defaults to this.universe)
|
|
483
|
+
*/
|
|
484
|
+
PointSource.prototype.repositionToRect = function (l, t, w, h, universe) {
|
|
485
|
+
var u = universe || this.universe;
|
|
486
|
+
var cx = l + w / 2;
|
|
487
|
+
var cy = t + h / 2;
|
|
488
|
+
var radius = this.diameter / 2;
|
|
489
|
+
var angle_increment = this.angle_increment;
|
|
490
|
+
var numAngles = Math.floor(360 / angle_increment);
|
|
491
|
+
|
|
492
|
+
// Update stored center so inspector reads current values.
|
|
493
|
+
this.x = cx;
|
|
494
|
+
this.y = cy;
|
|
495
|
+
// The marker this source owns follows its ONE position write (a diameter disc at the centre).
|
|
496
|
+
if (this._marker) {
|
|
497
|
+
this._marker.setRect([Math.round(cx - this.diameter / 2), Math.round(cy - this.diameter / 2),
|
|
498
|
+
this.diameter, this.diameter]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Build the angle-indexed source rects (same for all groups — co-located).
|
|
502
|
+
var newRects = [];
|
|
503
|
+
for (var i = 0; i < numAngles; i++) {
|
|
504
|
+
var angleDeg = i * angle_increment;
|
|
505
|
+
var angleRad = angleDeg * Math.PI / 180;
|
|
506
|
+
var emitX = Math.round(cx + radius * Math.cos(angleRad));
|
|
507
|
+
var emitY = Math.round(cy + radius * Math.sin(angleRad));
|
|
508
|
+
newRects.push([emitX, emitY, 1, 1]);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Update handles in ALL registered groups (active and inactive alike —
|
|
512
|
+
// inactive groups must stay positioned correctly for when they are re-activated).
|
|
513
|
+
var allGroupKeys = Object.keys(this._handleGroups);
|
|
514
|
+
for (var gi = 0; gi < allGroupKeys.length; gi++) {
|
|
515
|
+
var groupHandles = this._handleGroups[allGroupKeys[gi]];
|
|
516
|
+
for (var hi = 0; hi < groupHandles.length; hi++) {
|
|
517
|
+
// groupHandles is ordered angle0..angleN-1 (same order as newRects).
|
|
518
|
+
var angleIdx = hi % numAngles;
|
|
519
|
+
groupHandles[hi].sourceRects = [newRects[angleIdx]]; // mutate sugar-owned emState
|
|
520
|
+
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Full teardown (Studio Controls Realignment S4, design item 1 — the
|
|
527
|
+
* SAME "delete leaves the tick-fn running" defect class found on
|
|
528
|
+
* QuantumEmitter, closed here too for the sibling composite emitter type).
|
|
529
|
+
* Tombstones + deactivates every emState across ALL registered (charge, rd)
|
|
530
|
+
* groups (active or not — a re-activated inactive group must not resurrect),
|
|
531
|
+
* then physically splices this PointSource's host tick-fn(s) out of
|
|
532
|
+
* `universe.experimentTickFunctions` via the `_emState` back-reference
|
|
533
|
+
* stashed in `_ensureGroup`. Idempotent — a second call is a safe no-op.
|
|
534
|
+
*/
|
|
535
|
+
PointSource.prototype.dispose = function () {
|
|
536
|
+
if (this._disposed) { return; }
|
|
537
|
+
this._disposed = true;
|
|
538
|
+
|
|
539
|
+
var mine = [];
|
|
540
|
+
var allGroupKeys = Object.keys(this._handleGroups);
|
|
541
|
+
for (var gi = 0; gi < allGroupKeys.length; gi++) {
|
|
542
|
+
var groupHandles = this._handleGroups[allGroupKeys[gi]];
|
|
543
|
+
for (var hi = 0; hi < groupHandles.length; hi++) {
|
|
544
|
+
groupHandles[hi].tombstoned = true;
|
|
545
|
+
groupHandles[hi].active = false;
|
|
546
|
+
mine.push(groupHandles[hi]);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (this.universe && Array.isArray(this.universe.experimentTickFunctions)) {
|
|
551
|
+
var fns = this.universe.experimentTickFunctions;
|
|
552
|
+
for (var j = fns.length - 1; j >= 0; j--) {
|
|
553
|
+
if (fns[j]._emState && mine.indexOf(fns[j]._emState) !== -1) {
|
|
554
|
+
fns.splice(j, 1);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// The marker is this emitter's (attachMarker): it leaves the world with it.
|
|
559
|
+
if (this._marker && typeof this._marker.dispose === 'function') { this._marker.dispose(); }
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* attachMarker(lo) — take ownership of the Design marker (the NONE-collision circle LargeObject
|
|
564
|
+
* the ObjectFactory builder draws at this emitter). Called ONCE, by that builder. Stamps the ONE
|
|
565
|
+
* up-link (`lo._owner = this`, so a pick on the disc resolves to THIS emitter) and keeps the one
|
|
566
|
+
* down-link (`_marker`) this emitter moves it and disposes it through. Zero physics.
|
|
567
|
+
* @param {LargeObject} lo @returns {PointSource} this
|
|
568
|
+
*/
|
|
569
|
+
PointSource.prototype.attachMarker = function (lo) {
|
|
570
|
+
if (!lo || typeof lo !== 'object') {
|
|
571
|
+
throw new TypeError('[PointSource.attachMarker] needs the marker LargeObject. (FAIL-LOUD.)');
|
|
572
|
+
}
|
|
573
|
+
if (this._marker) {
|
|
574
|
+
throw new Error('[PointSource.attachMarker] this emitter already owns a marker. (FAIL-LOUD.)');
|
|
575
|
+
}
|
|
576
|
+
this._marker = lo;
|
|
577
|
+
lo._owner = this;
|
|
578
|
+
return this;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// ── The kind (objects/kinds.js): descriptor, construction defaults, builder ──────────
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* PointSource — Radial emitter (one engine emitter handle per angle).
|
|
585
|
+
* Constructor: PointSource(universe, x, y, diameter, speed, charge,
|
|
586
|
+
* auto_fire, quantum_limit, delay, angle_increment, opts)
|
|
587
|
+
* opts fields: { emergentPhysics, minRadius, color, wavelength }
|
|
588
|
+
*/
|
|
589
|
+
var PointSourceDescriptor = {
|
|
590
|
+
description: 'A radial point emitter that fires particles outward from the surface ' +
|
|
591
|
+
'of a circle at regular angular intervals. Each angle gets its own engine emitter ' +
|
|
592
|
+
'handle so the angular pattern is deterministic. Produces wave fronts, interference ' +
|
|
593
|
+
'patterns, and Big Bang-style expansions depending on configuration.',
|
|
594
|
+
fields: [
|
|
595
|
+
{
|
|
596
|
+
name: 'rect',
|
|
597
|
+
type: 'rect',
|
|
598
|
+
engineVisible: true,
|
|
599
|
+
description: 'Canonical bounding rect ' + ValueNormalizer.rectTupleText(4) + ' — repositions center (cx=left+w/2, cy=top+h/2) and recomputes all angle source rects (one-path via repositionToRect)'
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'x',
|
|
603
|
+
type: 'number',
|
|
604
|
+
constraint: { integer: true },
|
|
605
|
+
engineVisible: true,
|
|
606
|
+
description: 'Center X of the point source (physics pixels)'
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: 'y',
|
|
610
|
+
type: 'number',
|
|
611
|
+
constraint: { integer: true },
|
|
612
|
+
engineVisible: true,
|
|
613
|
+
description: 'Center Y of the point source (physics pixels)'
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'diameter',
|
|
617
|
+
type: 'number',
|
|
618
|
+
constraint: { min: 1, integer: true },
|
|
619
|
+
engineVisible: true,
|
|
620
|
+
unit: 'px',
|
|
621
|
+
description: 'Diameter of the emission circle (physics pixels)'
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: 'speed',
|
|
625
|
+
type: 'number',
|
|
626
|
+
constraint: { min: 0, integer: false },
|
|
627
|
+
engineVisible: true,
|
|
628
|
+
unit: 'px/tick',
|
|
629
|
+
description: 'Velocity magnitude for emitted particles (pixels/tick)'
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: 'auto_fire',
|
|
633
|
+
type: 'boolean',
|
|
634
|
+
engineVisible: true,
|
|
635
|
+
description: 'Whether all angle emitters fire automatically'
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: 'quantum_limit',
|
|
639
|
+
type: 'number',
|
|
640
|
+
constraint: { min: 1, integer: true },
|
|
641
|
+
engineVisible: true,
|
|
642
|
+
description: 'Max quanta per angle emitter (null = Infinity)'
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: 'delay',
|
|
646
|
+
type: 'number',
|
|
647
|
+
constraint: { min: 0, integer: true },
|
|
648
|
+
engineVisible: true,
|
|
649
|
+
description: 'Ticks between bursts for each angle emitter'
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'angle_increment',
|
|
653
|
+
type: 'number',
|
|
654
|
+
constraint: { min: 1, max: 360, integer: false },
|
|
655
|
+
engineVisible: true,
|
|
656
|
+
description: 'Degrees between emission angles. Fewer degrees = more angles = denser wave front. ' +
|
|
657
|
+
'5° gives 72 directions; 10° gives 36; 45° gives 8 (coarser).'
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: 'colorOverride',
|
|
661
|
+
type: 'rgb',
|
|
662
|
+
engineVisible: false,
|
|
663
|
+
description: 'Override render color for emitted particles [r, g, b] (render-only; stored on the emitter wrapper).'
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'chargeMode',
|
|
667
|
+
type: 'enum',
|
|
668
|
+
constraint: { values: { random: 'random', positive: 'positive', negative: 'negative', both: 'both' } },
|
|
669
|
+
engineVisible: false,
|
|
670
|
+
description: 'High-level charge mode (sugar layer). "both" creates two per-angle handle groups ' +
|
|
671
|
+
'(POSITIVE + NEGATIVE), doubling the emission count. Toggles the handle active flag — no engine ABI change.'
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
name: 'random_direction',
|
|
675
|
+
type: 'boolean',
|
|
676
|
+
engineVisible: false,
|
|
677
|
+
description: 'Whether emitted particles have randomized direction (sugar layer). ' +
|
|
678
|
+
'PointSource normally uses directed emission; enabling this registers a new handle group with rd=true.'
|
|
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). Stored on the emitter wrapper; no engine ABI change required.'
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
//
|
|
691
|
+
// Reconstructs both the PointSource emitter AND a visual circle LargeObject
|
|
692
|
+
// handle (so the restored source is visible and hand-draggable, just like a
|
|
693
|
+
// QuantumEmitter). The marker is the source's x/y/diameter, owned by the source.
|
|
694
|
+
registerKind(PointSource, {
|
|
695
|
+
from: import.meta.url,
|
|
696
|
+
descriptor: PointSourceDescriptor,
|
|
697
|
+
defaults: {
|
|
698
|
+
x: 50, y: 100,
|
|
699
|
+
diameter: 16,
|
|
700
|
+
speed: 1,
|
|
701
|
+
chargeMode: 'random',
|
|
702
|
+
auto_fire: true,
|
|
703
|
+
quantum_limit: null,
|
|
704
|
+
delay: 0,
|
|
705
|
+
angle_increment: 10
|
|
706
|
+
},
|
|
707
|
+
build: function (props, ctx, Class) {
|
|
708
|
+
var PS = Class; // the class that registered (a composed fork's own class)
|
|
709
|
+
var LO3 = LargeObject;
|
|
710
|
+
var CT3 = CollisionTypes;
|
|
711
|
+
var psCx = props.x != null ? props.x : Math.round(ctx.x);
|
|
712
|
+
var psCy = props.y != null ? props.y : Math.round(ctx.y);
|
|
713
|
+
var psDiam = props.diameter || 16;
|
|
714
|
+
// Bug #1 fix: pass null as raw charge; set chargeMode via the sugar setter AFTER
|
|
715
|
+
// construction. The schema only captures chargeMode (not raw charge) now.
|
|
716
|
+
var ps = new PS(
|
|
717
|
+
ctx.runner.universe,
|
|
718
|
+
psCx,
|
|
719
|
+
psCy,
|
|
720
|
+
psDiam,
|
|
721
|
+
props.speed != null ? props.speed : 1,
|
|
722
|
+
null, // raw charge — overridden immediately below via chargeMode setter
|
|
723
|
+
props.auto_fire != null ? props.auto_fire : true,
|
|
724
|
+
props.quantum_limit != null ? props.quantum_limit : Infinity,
|
|
725
|
+
props.delay != null ? props.delay : 0,
|
|
726
|
+
props.angle_increment || 10,
|
|
727
|
+
{
|
|
728
|
+
// Re-pass the construction opts captured by serializeDesign so the
|
|
729
|
+
// rebuilt emitter keeps its wavelength (→ correct leaf mass / particle
|
|
730
|
+
// size + color), particle color, and emergent-physics mode.
|
|
731
|
+
wavelength: props._wavelength != null ? props._wavelength : null,
|
|
732
|
+
color: props._optColor || null,
|
|
733
|
+
emergentPhysics: props._emergentPhysics || false
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
ps._schemaKind = Class.name;
|
|
737
|
+
// Restore chargeMode via the sugar setter (handles 'random'|'positive'|'negative'|'both').
|
|
738
|
+
if (props.chargeMode) { ps.chargeMode = props.chargeMode; }
|
|
739
|
+
if (props.colorOverride) { ps.colorOverride = props.colorOverride; }
|
|
740
|
+
|
|
741
|
+
// Visual handle: circle LargeObject at the source position (its x/y/diameter).
|
|
742
|
+
var psVLeft = Math.round(psCx - psDiam / 2), psVTop = Math.round(psCy - psDiam / 2);
|
|
743
|
+
var psVW = psDiam, psVH = psDiam;
|
|
744
|
+
var psName = props.name || ('source ' + declaredCount(ctx.runner, 'PointSource'));
|
|
745
|
+
ps.name = psName; // the declared name is the source's; its marker wears it
|
|
746
|
+
var psVis = new LO3(ctx.runner.universe,
|
|
747
|
+
[psVLeft, psVTop, psVW, psVH],
|
|
748
|
+
CT3.NONE, [],
|
|
749
|
+
psName,
|
|
750
|
+
'closest', null, 1,
|
|
751
|
+
[0, 0, 0]);
|
|
752
|
+
psVis.drawAsCircle = true;
|
|
753
|
+
psVis._schemaKind = 'LargeObject';
|
|
754
|
+
ps.attachMarker(psVis);
|
|
755
|
+
return ps;
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// ── Export ───────────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
export { PointSource };
|
|
762
|
+
|