@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p1.1
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 +2 -2
- package/README.md +7 -3
- package/dist/dist-manifest.json +54 -48
- package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
- package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
- package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
- package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
- package/dist/frqtl/objects/FrqtlApp.js +109 -30
- package/dist/frqtl/objects/ObjectFactory.js +4 -5
- package/dist/frqtl/objects/RecipeContext.js +451 -9
- package/dist/frqtl/objects/instruments/hosted-eyes.js +68 -28
- package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
- package/dist/frqtl/objects/schema/index.js +72 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
- package/dist/frqtl/parts/Wall.js +2 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
- package/dist/frqtl/scenes/absorbing-wall.js +18 -14
- package/dist/frqtl/scenes/atomic-model.js +3 -0
- package/dist/frqtl/scenes/big-bang.js +3 -0
- package/dist/frqtl/scenes/blackbody.js +14 -11
- package/dist/frqtl/scenes/circuit-sat.js +7 -4
- package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
- package/dist/frqtl/scenes/counting-wall.js +19 -15
- package/dist/frqtl/scenes/double-slit.js +3 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
- package/dist/frqtl/scenes/dse-one-slit.js +59 -55
- package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
- package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
- package/dist/frqtl/scenes/dse-two-slits.js +69 -65
- package/dist/frqtl/scenes/empty.js +3 -0
- package/dist/frqtl/scenes/laser-buildup.js +23 -16
- package/dist/frqtl/scenes/mirror-3d.js +31 -27
- package/dist/frqtl/scenes/mirror-route.js +4 -1
- package/dist/frqtl/scenes/mirror.js +24 -20
- package/dist/frqtl/scenes/omni-3d.js +7 -4
- package/dist/frqtl/scenes/omni-source.js +7 -4
- package/dist/frqtl/scenes/self-interference.js +13 -10
- package/dist/frqtl/scenes/smoking-gun.js +15 -11
- package/dist/frqtl/scenes/straight-walk.js +7 -4
- package/dist/frqtl/scenes/sun.js +11 -8
- package/dist/frqtl/scenes/two-source.js +22 -17
- package/dist/frqtl/scenes/wave.js +10 -7
- package/dist/shim/bundle/manifest.js +1 -1
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/page-params/index.js +2 -2
- package/dist/shim/project/index.js +7 -5
- package/dist/shim/project/link.js +12 -5
- package/dist/shim/project/parts-module.js +67 -0
- package/dist/shim/project/parts-writer.js +453 -0
- package/dist/shim/project/specifiers.js +41 -13
- package/dist/shim/registry/walkthrough.js +9 -9
- package/dist/shim/render-state/apply.js +11 -14
- package/llms.txt +2 -2
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
|
|
7
7
|
* (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
|
|
8
|
-
* the parts (`exp.addObject`) and the render intent are all here; the sugar
|
|
8
|
+
* the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
|
|
9
9
|
* objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
|
|
10
10
|
* (rung dse-tunnel-rebuild).
|
|
11
11
|
*/
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// The double slit built on the TUNNEL OBJECT at physical DSE proportions. The two
|
|
14
14
|
// slit channels are Tunnels — TWO parallel MIRROR walls (guarded bounce, zero leak) — so
|
|
15
15
|
// the slit reflects the beam; the barriers + screen are v2 Walls (destroy / detector-screen
|
|
16
|
-
// preset) so
|
|
16
|
+
// preset), declared in `parts` so a hand edit is a code edit. GEOMETRY: SYMMETRIC —
|
|
17
17
|
// emitter->front of the slit wall equals back of the slit wall->detector. The near-field
|
|
18
18
|
// result is a balanced, central-dominant three-lobe double-slit interference pattern (a
|
|
19
19
|
// stochastic near-field walk; the ray-optics λL/d Fraunhofer regime needs L >> d²/λ ≈ 580px,
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
//
|
|
22
22
|
// ONE MODE-AWARE SOURCE (S3/W2). The mode strip
|
|
23
23
|
// selects the RENDERER the host boots (renderState.renderer), and this scene reads it ONCE,
|
|
24
|
-
// before its world: `three` ⇒ exp.createScene (a 3D world), else exp.createCanvas (2D).
|
|
25
|
-
//
|
|
24
|
+
// before its world: `three` ⇒ exp.createScene (a 3D world), else exp.createCanvas (2D).
|
|
25
|
+
// `parts` is written in the DEFAULT mode, 3D; in 2D the scene declares flat2D(parts), the
|
|
26
|
+
// scene's own statement of its 2D delta — there is NO second (`-3d`) source file:
|
|
26
27
|
// * the emitter opens the z-axis (is2D:!IS3D → activeAxes.z, a 3D ball of directions);
|
|
27
28
|
// * every barrier / tunnel / detector gains the SAME z-slab (z/depth → a 6-element rect),
|
|
28
29
|
// so the slit wall is a real SLOT-in-a-PLATE and what you SEE is what collides;
|
|
@@ -40,108 +41,98 @@
|
|
|
40
41
|
// (= mass/capacity = 1/wavelength). See the emitter block in the source for the two
|
|
41
42
|
// independent dials (band vs pixel wavelength) and why the fringe geometry is unchanged.
|
|
42
43
|
|
|
43
|
-
import { CameraControls, FrqtlControlOverlay, FrqtlControlsPanel, SphereEmitter } from '@descix/frqtl-web/parts';
|
|
44
|
+
import { CameraControls, FrqtlControlOverlay, FrqtlControlsPanel, GraphingDetectorWall, SphereEmitter, Tunnel, Wall } from '@descix/frqtl-web/parts';
|
|
44
45
|
import * as ControlRegistry from '@descix/frqtl-web/shim/controls/registry.js';
|
|
45
46
|
import * as FrqtlDisplay from '@descix/frqtl-web/compat/renderer/display-slot.js';
|
|
46
47
|
import * as SunLook from '@descix/frqtl-web/shim/render/sun-look.js';
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// RESTRICTED to the forward half — same physics, pointed one way, so nothing about the
|
|
122
|
-
// walk changes. Forward is +X, which is where the slit wall stands (orientationDeg stays
|
|
123
|
-
// 0), so the half of the burst that used to fly LEFT into the back-scatter sink now goes
|
|
124
|
-
// toward the slits: the same burst size illuminates the aperture roughly twice as hard.
|
|
125
|
-
var SRC = {
|
|
126
|
-
preset: 'hemisphere', is2D: !IS3D, center: [srcX, Math.round(gcY)],
|
|
49
|
+
const CW = 600, H = 400, WORLD_D = 600; // THE WORLD this scene states (its co-init call below)
|
|
50
|
+
// GEOMETRY and WAVELENGTH are two INDEPENDENT dials, and they are kept apart on
|
|
51
|
+
// purpose: `wl` sizes the PLATE (slit height, gap, wall placement) and nothing else,
|
|
52
|
+
// while `lamPx` is the transverse wavelength in pixels. Doubling lambda must NOT move
|
|
53
|
+
// the slit wall — if one variable drove both, the aperture would silently walk every
|
|
54
|
+
// time the beam was re-tuned and no two runs would be comparable.
|
|
55
|
+
const wl = 64, sh = Math.floor(wl * 0.8), gap = Math.floor(wl * 0.8); // 51, 51
|
|
56
|
+
const lamPx = 128; // the TRANSVERSE wavelength, in pixels
|
|
57
|
+
const groupH = sh + gap + sh; // 153
|
|
58
|
+
const gcY = H / 2; // 300 @ H=600
|
|
59
|
+
const s1Top = gcY - groupH / 2; // 223.5
|
|
60
|
+
const gapTop = s1Top + sh; // 274.5
|
|
61
|
+
const s2Bot = gcY + groupH / 2; // 376.5
|
|
62
|
+
const s2Top = s2Bot - sh; // 325.5
|
|
63
|
+
const srcX = 20; // source x (left, near the slit wall)
|
|
64
|
+
const L = wl * 1.5, W = wl; // slit-group left 96, width 64 (wall front..back = [96,160])
|
|
65
|
+
// THE DETECTOR LEG, DERIVED FROM THE RING LADDER (zero free parameters).
|
|
66
|
+
// A phase-locked POINT source lays down density rings at r_m = (lambda/4)*sqrt((2m+1)^2+1)
|
|
67
|
+
// measured from the emission point (guide row r3-radius-stall-shells). Each slit exit below
|
|
68
|
+
// IS such a source, so the plate should be placed where
|
|
69
|
+
// a NAMED ring crosses it rather than at a symmetric figure. Put the MIDLINE on ring m=2
|
|
70
|
+
// of BOTH slits: with the exits at +/-halfSep from the midline, that needs a leg of
|
|
71
|
+
// D = sqrt(r_2^2 - halfSep^2) = sqrt(163.2^2 - 51^2) = 155
|
|
72
|
+
// so the two ring-2 circles intersect exactly on the axis and the centre is DOUBLY
|
|
73
|
+
// reinforced. Rings inside D (m=0 at 45.3, m=1 at 101.2) never reach the plate and
|
|
74
|
+
// contribute no band — a measured property of the ladder, not an assumption.
|
|
75
|
+
// This REPLACES the old symmetric-leg figure, whose own comment made it conditional on
|
|
76
|
+
// the slits becoming directed emitters; the exit gates below are that condition.
|
|
77
|
+
const ringR = function (m) { return (lamPx / 4) * Math.sqrt((2 * m + 1) * (2 * m + 1) + 1); };
|
|
78
|
+
const slitCY1 = s1Top + sh / 2, slitCY2 = s2Top + sh / 2; // 249, 351 — the bore midlines
|
|
79
|
+
const halfSep = gcY - slitCY1; // 51
|
|
80
|
+
const detW = 20; // the detector plate's own thickness
|
|
81
|
+
const D = Math.round(Math.sqrt(ringR(2) * ringR(2) - halfSep * halfSep)); // 155
|
|
82
|
+
const detX = (L + W) + D; // 315
|
|
83
|
+
const linerH = Math.min(10, Math.floor(sh * 0.2)), bore = sh - 2 * linerH; // 10, 31
|
|
84
|
+
// THE 3D DELTA: the plate z-band. Every barrier / tunnel liner / detector in `parts`
|
|
85
|
+
// carries the SAME z-slab (z: -zHalf, depth: 2 * zHalf), so the slit wall is a genuine SLOT
|
|
86
|
+
// IN A PLATE standing in space (the slits are slots long in z — the 2D figure extruded).
|
|
87
|
+
// flat2D (below) deletes the slab, so the 2D scene is the flat figure.
|
|
88
|
+
const zHalf = Math.round(groupH / 2); // 77 @ wl 64
|
|
89
|
+
// THE BEAM'S COLOUR — its own frequency band, never a forced recolor. The emitter
|
|
90
|
+
// emits COLOURLESS leaves (no renderColor arg ⇒ no string_map['render.color']), so the
|
|
91
|
+
// renderer derives the colour from the ONE authority, LightColor, at the leaf's
|
|
92
|
+
// fullness. TWO INDEPENDENT dials produce it, both on the emitter:
|
|
93
|
+
// fullness = mass/capacity = 1 / wavelength -> the spectral BAND (LightColor)
|
|
94
|
+
// lambda_px = wavelength x wavelengthScale -> the transverse wave, in PIXELS
|
|
95
|
+
// (wavelengthScale = floor(fundamentalWaveLength / 4)). So the band is chosen by
|
|
96
|
+
// scaling fundamentalWaveLength INVERSELY with the wavelength arg — lambda_px (the
|
|
97
|
+
// fringe geometry) is untouched. 1/3.2 = 0.3125 sits in the YELLOW band; the rung's
|
|
98
|
+
// renderState declares the spectrally correct 'roygbiv' ramp, so the beam renders
|
|
99
|
+
// [255,243,0]. lambda_px = 3.2 x 40 = 128, INDEPENDENT of the 64-derived plate.
|
|
100
|
+
// THIS BEAM IS LIGHT, so the band is free: these leaves are born AT the universe's
|
|
101
|
+
// fundamental dimension (leafDimension == fundamentalDimension == 2), and a leaf at the
|
|
102
|
+
// fundamental ALWAYS steps, P=1, whatever its fullness (bench M1) — the fullness
|
|
103
|
+
// move-gate (bench M2) simply does not apply, and P=1 draws no RNG. Measured on the
|
|
104
|
+
// live detector: the fringe histogram is bin-for-bin the SAME as the (64, 16) tuple
|
|
105
|
+
// (t=30s, 16px profile: 11260 vs 11262 hits, every bin within 1, COM 201.7 both).
|
|
106
|
+
const lamArg = 3.2; // fullness 1/3.2 = 0.3125 -> yellow
|
|
107
|
+
const fwl = 4 * (lamPx / lamArg); // 160 -> wavelengthScale 40 -> lambda_px = 128
|
|
108
|
+
|
|
109
|
+
// The light source — the current SphereEmitter sugar (braided walk), placed at the
|
|
110
|
+
// original source pixel (srcX, gap-center = H/2).
|
|
111
|
+
// is2D:!IS3D opens activeAxes.z — the ball-rejected sampler then draws a 3D BALL of
|
|
112
|
+
// directions (bench S1/S5) instead of the 2D disk.
|
|
113
|
+
// preset 'hemisphere' = 180 degrees of FORWARD emission (D1): the SAME ball-rejected
|
|
114
|
+
// sampler with the forward axis' range halved to [0,R], i.e. the spherical distribution
|
|
115
|
+
// RESTRICTED to the forward half — same physics, pointed one way, so nothing about the
|
|
116
|
+
// walk changes. Forward is +X, which is where the slit wall stands (orientationDeg stays
|
|
117
|
+
// 0), so the half of the burst that used to fly LEFT into the back-scatter sink now goes
|
|
118
|
+
// toward the slits: the same burst size illuminates the aperture roughly twice as hard.
|
|
119
|
+
export const parts = {
|
|
120
|
+
source: [SphereEmitter, {
|
|
121
|
+
preset: 'hemisphere', is2D: false, center: [srcX, Math.round(gcY)],
|
|
127
122
|
chargeMode: 'random', wavelength: lamArg, burstSize: 8, emitEveryTicks: 1,
|
|
128
123
|
fundamentalDimension: 2, fundamentalWaveLength: fwl
|
|
129
|
-
}
|
|
130
|
-
// THE WORLD — first, then parts: a 3D scene + universe (600x400x600) or a 2D canvas + universe.
|
|
131
|
-
if (IS3D) { exp.createScene(CW, H, WORLD_D, SphereEmitter.worldCfg(SRC)); }
|
|
132
|
-
else { exp.createCanvas(CW, H, SphereEmitter.worldCfg(SRC)); }
|
|
133
|
-
exp.addObject('SphereEmitter', SRC);
|
|
124
|
+
}],
|
|
134
125
|
// Start wall — a plain sink (collisionBehavior:'destroy', [set is_alive=0]) at the
|
|
135
126
|
// left edge, catching back-scatter. It is TRANSPARENT (alpha 40 of 255): the default
|
|
136
127
|
// camera stands BEHIND the emitter, so an opaque slab here would be the first thing in
|
|
137
|
-
// frame and would hide the whole experiment. alpha is a DECLARED Wall field, so
|
|
138
|
-
//
|
|
128
|
+
// frame and would hide the whole experiment. alpha is a DECLARED Wall field, so the parts block
|
|
129
|
+
// carries it and Restart rebuilds the wall just as sheer. Physics is untouched — the
|
|
139
130
|
// sink still destroys every back-scattered quantum (render-only, SU1).
|
|
140
|
-
|
|
131
|
+
start_wall: [Wall, { rect: [0, 0, 10, H], z: -zHalf, depth: 2 * zHalf, collisionBehavior: 'destroy', color: [120, 120, 120], alpha: 40 }],
|
|
141
132
|
// Barrier segments — opaque plain sinks (destroy) leaving the two slit gaps.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
133
|
+
wall1: [Wall, { rect: [L, 0, W, s1Top - 1], z: -zHalf, depth: 2 * zHalf, collisionBehavior: 'destroy', color: [200, 60, 60] }],
|
|
134
|
+
wall2: [Wall, { rect: [L, gapTop, W, gap - 1], z: -zHalf, depth: 2 * zHalf, collisionBehavior: 'destroy', color: [150, 40, 40] }],
|
|
135
|
+
wall3: [Wall, { rect: [L, s2Bot, W, (H - s2Bot) - 1], z: -zHalf, depth: 2 * zHalf, collisionBehavior: 'destroy', color: [200, 60, 60] }],
|
|
145
136
|
// The two slit channels as TUNNELS — walls land at the EXACT original liner positions
|
|
146
137
|
// ([L,s1Top,W,10] & [L,s1Top+sh-10,W,10]); the bore is the clear aperture between them.
|
|
147
138
|
// The Tunnel ferries z/depth onto BOTH walls, so a 3D channel is a real corridor.
|
|
@@ -168,8 +159,8 @@ export default function buildExperiment(exp) {
|
|
|
168
159
|
// Unifying this with the SphereEmitter preset vocabulary (one owner, ferried — never a
|
|
169
160
|
// second preset list) is an open contract design;
|
|
170
161
|
// this rung uses the existing flags and documents the mapping.
|
|
171
|
-
|
|
172
|
-
|
|
162
|
+
slit1_channel: [Tunnel, { left: L, top: s1Top + linerH, length: W, bore: bore, wallThickness: linerH, axis: 'x', z: -zHalf, depth: 2 * zHalf, mode: 'directed', exitCollapse: true, exitRecenter: true, exitPhaseReset: true, color: [80, 140, 220] }],
|
|
163
|
+
slit2_channel: [Tunnel, { left: L, top: s2Top + linerH, length: W, bore: bore, wallThickness: linerH, axis: 'x', z: -zHalf, depth: 2 * zHalf, mode: 'directed', exitCollapse: true, exitRecenter: true, exitPhaseReset: true, color: [80, 140, 220] }],
|
|
173
164
|
// The detector SCREEN — a NON-ABSORBING COUNTING wall ('transparent-probe':
|
|
174
165
|
// collisionBehavior 'nothing' + the count hook) in BOTH modes. This is the
|
|
175
166
|
// |psi|^2-faithful screen: an ABSORBING plate removes each quantum at first contact,
|
|
@@ -187,8 +178,42 @@ export default function buildExperiment(exp) {
|
|
|
187
178
|
// FAR-EDGE SINK. The screen below COUNTS WITHOUT ABSORBING, so quanta fly on past it;
|
|
188
179
|
// this destroy wall at the right edge is the experiment-boundary sink that keeps the
|
|
189
180
|
// population bounded (Invariant 5/7 — only an experiment boundary may destroy).
|
|
190
|
-
|
|
191
|
-
|
|
181
|
+
far_sink: [Wall, { rect: [CW - 10, 0, 10, H], z: -zHalf, depth: 2 * zHalf, collisionBehavior: 'destroy', color: [120, 120, 120], alpha: 40 }],
|
|
182
|
+
screen: [GraphingDetectorWall, { rect: [detX, 0, detW, H], z: -zHalf, depth: 2 * zHalf, preset: 'transparent-probe', color: [120, 200, 120], alpha: 220, binPx: 12 }],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* flat2D(parts) — THE 2D MODE of this scene, derived from its 3D `parts`: the same keys in the same order,
|
|
187
|
+
* each entry's props copied with the z-slab (z, depth) deleted, is2D set true (by assignment, so the key
|
|
188
|
+
* keeps its position), and the GraphingDetectorWall plate a plain counting Wall without its paint
|
|
189
|
+
* (alpha, binPx). Pure: it reads only its argument. A hand-added entry flattens by the same rule.
|
|
190
|
+
*/
|
|
191
|
+
function flat2D(parts) {
|
|
192
|
+
var out = {};
|
|
193
|
+
Object.keys(parts).forEach(function (key) {
|
|
194
|
+
var K = parts[key][0];
|
|
195
|
+
var props = Object.assign({}, parts[key][1]);
|
|
196
|
+
delete props.z;
|
|
197
|
+
delete props.depth;
|
|
198
|
+
if ('is2D' in props) { props.is2D = true; }
|
|
199
|
+
if (K === GraphingDetectorWall) { K = Wall; delete props.alpha; delete props.binPx; }
|
|
200
|
+
out[key] = [K, props];
|
|
201
|
+
});
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default function buildExperiment(exp) {
|
|
206
|
+
// Double Slit (Tunnel) — the two slit channels are MIRROR TUNNELS (guarded
|
|
207
|
+
// bounce, zero-leak); the detector sits at the SYMMETRIC distance (emitter->front ==
|
|
208
|
+
// back->detector). X placements are wl-derived (canvas-width-independent); Y is
|
|
209
|
+
// centered on H (groupCenterY = H/2).
|
|
210
|
+
// The booted display (the host's renderState.renderer) decides which world this mode makes.
|
|
211
|
+
var IS3D = FrqtlDisplay.name() === 'three';
|
|
212
|
+
var P = IS3D ? parts : flat2D(parts);
|
|
213
|
+
// THE WORLD — first, then parts: a 3D scene + universe (600x400x600) or a 2D canvas + universe.
|
|
214
|
+
if (IS3D) { exp.createScene(CW, H, WORLD_D, SphereEmitter.worldCfg(P.source[1])); }
|
|
215
|
+
else { exp.createCanvas(CW, H, SphereEmitter.worldCfg(P.source[1])); }
|
|
216
|
+
exp.declare(P);
|
|
192
217
|
if (IS3D) {
|
|
193
218
|
// SCENE Z-COLLIMATORS — two INVISIBLE full-canvas destroy boundaries flush OUTSIDE
|
|
194
219
|
// the plate z-band (the cl1a-mirror-3d pattern): the emitted ball's z-tail diffuses,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
|
|
7
7
|
* (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
|
|
8
|
-
* the parts (`exp.addObject`) and the render intent are all here; the sugar
|
|
8
|
+
* the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
|
|
9
9
|
* objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
|
|
10
10
|
* (rung dse-two-emitters).
|
|
11
11
|
*/
|
|
@@ -71,19 +71,70 @@
|
|
|
71
71
|
// the title lines and the `var NSRC` line — lib/frqtl/scenes/test/scene-files.test.mjs
|
|
72
72
|
// fails if any other line diverges, so the A/B the pair exists to make stays an A/B.
|
|
73
73
|
|
|
74
|
-
import { SphereEmitter } from '@descix/frqtl-web/parts';
|
|
74
|
+
import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
|
|
75
|
+
|
|
76
|
+
const CW = 600, H = 400; // THE WORLD this scene states (its co-init call below)
|
|
77
|
+
// ── THE ONLY LENGTH ─────────────────────────────────────────────────────
|
|
78
|
+
// lamPx is lambda_eff in pixels. 320 is chosen for RESOLUTION, not for how many
|
|
79
|
+
// orders fit: the feature this rung exists to show (the minimum just inside the
|
|
80
|
+
// band and the peak riding outside it) is 0.025*lambda wide, which is 8 px at
|
|
81
|
+
// lambda 320 and 3 px at lambda 128. A stage that cannot separate them shows a
|
|
82
|
+
// smooth hump and leaves you unable to say whether the physics or the sampling
|
|
83
|
+
// was at fault. One well-resolved pair beats three mushy ones.
|
|
84
|
+
const lamPx = 320;
|
|
85
|
+
const sinkW = Math.round(lamPx / 16); // 20 — far-edge sink thickness
|
|
86
|
+
|
|
87
|
+
// ── THE DETECTOR LEG — ONE NAMED CONSTANT ───────────────────────────────
|
|
88
|
+
// D = legFrac * lambda, measured from the SOURCE (there is no barrier, so the
|
|
89
|
+
// emitter itself is the reference point of the ladder). legFrac = 1/4 is the
|
|
90
|
+
// leg the ring formula was written for; see the block comment above.
|
|
91
|
+
const legFrac = 1 / 4;
|
|
92
|
+
const D = Math.round(lamPx * legFrac); // 80
|
|
93
|
+
const srcX = Math.round(lamPx / 16); // 20 — launch room; the canvas edge is the back sink
|
|
94
|
+
const detX = srcX + D; // 100 — the plate's FRONT FACE
|
|
95
|
+
// PLATE THICKNESS IS AN OPTICAL CHOICE UNDER A FLUX DETECTOR, not trim. A flux
|
|
96
|
+
// probe counts every tick a quantum overlaps it, so it integrates the density
|
|
97
|
+
// ACROSS its thickness — and the ring crest is a CIRCLE about the source, not a
|
|
98
|
+
// plane. At the m=0 band the crest's slope is dy/dx = -D/y = -1, so a slab t px
|
|
99
|
+
// thick smears the band over ~t px of y. Against an 8 px feature a lambda/8 (40
|
|
100
|
+
// px) plate would guarantee mush. lambda/64 = 5 px keeps the smear sub-feature
|
|
101
|
+
// and is still unjumpable (a quantum steps at most 2 px/axis/tick). A 'hits'
|
|
102
|
+
// plate would not care — it counts at first contact, i.e. at the front face —
|
|
103
|
+
// which is exactly why the reference H1 runs could afford a lambda/8 slab.
|
|
104
|
+
const detW = Math.round(lamPx / 64); // 5 — plate thickness
|
|
105
|
+
// ── THE PLATE — A COUNTING WALL THAT DOES NOT ABSORB ────────────────────
|
|
106
|
+
// ONE NAMED CONSTANT, so the reading can be swapped in a single edit and the two
|
|
107
|
+
// instruments compared rather than argued about:
|
|
108
|
+
// 'transparent-probe' = collisionBehavior 'nothing' + mark/count. FLUX. The
|
|
109
|
+
// quantum is counted on every tick it overlaps the plate and then flies on,
|
|
110
|
+
// so the reading is the OCCUPANCY INTEGRAL at the plate plane — the density
|
|
111
|
+
// at that plane. This is the shipped choice.
|
|
112
|
+
// 'detector-screen' = destroy + count. HITS. Each quantum is counted once, at
|
|
113
|
+
// arrival, and removed: a transit census, the headcount a photographic plate
|
|
114
|
+
// would report. It is what the H1 reference runs used.
|
|
115
|
+
// NEITHER ABSORBS. 'absorb' stores the arriving mass and re-radiates it — that is
|
|
116
|
+
// the photoelectric experiment, and it saturates.
|
|
117
|
+
const detectorPreset = 'transparent-probe';
|
|
118
|
+
|
|
119
|
+
export const parts = {
|
|
120
|
+
// FAR-EDGE SINK. The plate below counts WITHOUT removing, so quanta fly on past
|
|
121
|
+
// it; this is the experiment-boundary sink downstream of the plate. It is not
|
|
122
|
+
// load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
|
|
123
|
+
// is where the population bound is STATED rather than inferred.
|
|
124
|
+
far_sink: [Wall, { rect: [CW - sinkW, 0, sinkW, H], collisionBehavior: 'destroy',
|
|
125
|
+
color: [120, 120, 120], alpha: 40 }],
|
|
126
|
+
// markTag — the plate's PROVENANCE stamp. The mark hook fires ONLY when the
|
|
127
|
+
// collision trigger does (genuine overlap), whereas the engine buffers every
|
|
128
|
+
// frame inside the LO's mb_w-PADDED search rect whether it triggered or not. So a
|
|
129
|
+
// raw frame count off the buffer is a padded-occupancy reading and the
|
|
130
|
+
// mark-filtered count is the trigger-gated census: two different instruments, and
|
|
131
|
+
// an offline reader needs the tag to tell them apart.
|
|
132
|
+
screen: [Wall, { rect: [detX, 0, detW, H],
|
|
133
|
+
preset: detectorPreset, markTag: 'plate', color: [120, 200, 120] }],
|
|
134
|
+
};
|
|
75
135
|
|
|
76
136
|
export default function buildExperiment(exp) {
|
|
77
137
|
var NSRC = 2;
|
|
78
|
-
var CW = 600, H = 400; // THE WORLD this scene states (its co-init call below)
|
|
79
|
-
// ── THE ONLY LENGTH ─────────────────────────────────────────────────────
|
|
80
|
-
// lamPx is lambda_eff in pixels. 320 is chosen for RESOLUTION, not for how many
|
|
81
|
-
// orders fit: the feature this rung exists to show (the minimum just inside the
|
|
82
|
-
// band and the peak riding outside it) is 0.025*lambda wide, which is 8 px at
|
|
83
|
-
// lambda 320 and 3 px at lambda 128. A stage that cannot separate them shows a
|
|
84
|
-
// smooth hump and leaves you unable to say whether the physics or the sampling
|
|
85
|
-
// was at fault. One well-resolved pair beats three mushy ones.
|
|
86
|
-
var lamPx = 320;
|
|
87
138
|
var lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
|
|
88
139
|
var fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
|
|
89
140
|
var wlScale = Math.floor(fwl / 4); // 100
|
|
@@ -91,26 +142,7 @@ export default function buildExperiment(exp) {
|
|
|
91
142
|
throw new Error('[dse-h1] lamPx ' + lamPx + ' is not realisable as lambda_eff: the engine ' +
|
|
92
143
|
'can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
|
|
93
144
|
}
|
|
94
|
-
// ── THE DETECTOR LEG — ONE NAMED CONSTANT ───────────────────────────────
|
|
95
|
-
// D = legFrac * lambda, measured from the SOURCE (there is no barrier, so the
|
|
96
|
-
// emitter itself is the reference point of the ladder). legFrac = 1/4 is the
|
|
97
|
-
// leg the ring formula was written for; see the block comment above.
|
|
98
|
-
var legFrac = 1 / 4;
|
|
99
|
-
var D = Math.round(lamPx * legFrac); // 80
|
|
100
|
-
var srcX = Math.round(lamPx / 16); // 20 — launch room; the canvas edge is the back sink
|
|
101
145
|
var cy = Math.round(H / 2); // 200 — the source plane's midline
|
|
102
|
-
var detX = srcX + D; // 100 — the plate's FRONT FACE
|
|
103
|
-
// PLATE THICKNESS IS AN OPTICAL CHOICE UNDER A FLUX DETECTOR, not trim. A flux
|
|
104
|
-
// probe counts every tick a quantum overlaps it, so it integrates the density
|
|
105
|
-
// ACROSS its thickness — and the ring crest is a CIRCLE about the source, not a
|
|
106
|
-
// plane. At the m=0 band the crest's slope is dy/dx = -D/y = -1, so a slab t px
|
|
107
|
-
// thick smears the band over ~t px of y. Against an 8 px feature a lambda/8 (40
|
|
108
|
-
// px) plate would guarantee mush. lambda/64 = 5 px keeps the smear sub-feature
|
|
109
|
-
// and is still unjumpable (a quantum steps at most 2 px/axis/tick). A 'hits'
|
|
110
|
-
// plate would not care — it counts at first contact, i.e. at the front face —
|
|
111
|
-
// which is exactly why the reference H1 runs could afford a lambda/8 slab.
|
|
112
|
-
var detW = Math.round(lamPx / 64); // 5 — plate thickness
|
|
113
|
-
var sinkW = Math.round(lamPx / 16); // 20 — far-edge sink thickness
|
|
114
146
|
// TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
|
|
115
147
|
// multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
|
|
116
148
|
// the strongest case. At this leg it also puts a band of EACH source on the
|
|
@@ -137,33 +169,7 @@ export default function buildExperiment(exp) {
|
|
|
137
169
|
for (var si = 0; si < srcYs.length; si++) {
|
|
138
170
|
exp.addObject('SphereEmitter', Object.assign({}, SRC, { center: [srcX, Math.round(srcYs[si])] }));
|
|
139
171
|
}
|
|
140
|
-
|
|
141
|
-
// it; this is the experiment-boundary sink downstream of the plate. It is not
|
|
142
|
-
// load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
|
|
143
|
-
// is where the population bound is STATED rather than inferred.
|
|
144
|
-
exp.addObject('Wall', { rect: [CW - sinkW, 0, sinkW, H], collisionBehavior: 'destroy',
|
|
145
|
-
name: 'far_sink', color: [120, 120, 120], alpha: 40 });
|
|
146
|
-
// ── THE PLATE — A COUNTING WALL THAT DOES NOT ABSORB ────────────────────
|
|
147
|
-
// ONE NAMED CONSTANT, so the reading can be swapped in a single edit and the two
|
|
148
|
-
// instruments compared rather than argued about:
|
|
149
|
-
// 'transparent-probe' = collisionBehavior 'nothing' + mark/count. FLUX. The
|
|
150
|
-
// quantum is counted on every tick it overlaps the plate and then flies on,
|
|
151
|
-
// so the reading is the OCCUPANCY INTEGRAL at the plate plane — the density
|
|
152
|
-
// at that plane. This is the shipped choice.
|
|
153
|
-
// 'detector-screen' = destroy + count. HITS. Each quantum is counted once, at
|
|
154
|
-
// arrival, and removed: a transit census, the headcount a photographic plate
|
|
155
|
-
// would report. It is what the H1 reference runs used.
|
|
156
|
-
// NEITHER ABSORBS. 'absorb' stores the arriving mass and re-radiates it — that is
|
|
157
|
-
// the photoelectric experiment, and it saturates.
|
|
158
|
-
var detectorPreset = 'transparent-probe';
|
|
159
|
-
// markTag — the plate's PROVENANCE stamp. The mark hook fires ONLY when the
|
|
160
|
-
// collision trigger does (genuine overlap), whereas the engine buffers every
|
|
161
|
-
// frame inside the LO's mb_w-PADDED search rect whether it triggered or not. So a
|
|
162
|
-
// raw frame count off the buffer is a padded-occupancy reading and the
|
|
163
|
-
// mark-filtered count is the trigger-gated census: two different instruments, and
|
|
164
|
-
// an offline reader needs the tag to tell them apart.
|
|
165
|
-
var screen = exp.addObject('Wall', { rect: [detX, 0, detW, H],
|
|
166
|
-
preset: detectorPreset, markTag: 'plate', name: 'screen', color: [120, 200, 120] });
|
|
172
|
+
const p = exp.declare(parts);
|
|
167
173
|
// ── THE INSTRUMENT — the live count-by-y histogram, REGISTERED TO THE PLATE ──
|
|
168
174
|
// The panel takes the plate's OWN rect y-origin (0) and height (H), so the two
|
|
169
175
|
// are co-extensive on the bin axis at any canvas size, and it starts just past
|
|
@@ -189,7 +195,7 @@ export default function buildExperiment(exp) {
|
|
|
189
195
|
// profile's floor rises the curve compresses toward the left edge. Read positions off
|
|
190
196
|
// it, not magnitudes; magnitudes come from getCollisionHistogram().
|
|
191
197
|
exp.addObject('LiveChart', {
|
|
192
|
-
source: screen, orientation: 90,
|
|
198
|
+
source: p.screen, orientation: 90,
|
|
193
199
|
rect: [detX + detW + 8, 0, Math.max(60, CW - (detX + detW + 8) - 8), H],
|
|
194
200
|
series: 'line', label: 'count by y', refreshMs: 100, color: [24, 32, 46], alpha: 225
|
|
195
201
|
});
|