@descix/frqtl-node 0.6.0-p1.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +6 -4
  3. package/dist/dist-manifest.json +71 -65
  4. package/dist/eyes/index.js +5 -2
  5. package/dist/fat/pkg-node/package.json +2 -10
  6. package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
  7. package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
  8. package/dist/frqtl/objects/FrqtlApp.js +33 -6
  9. package/dist/frqtl/objects/RecipeContext.js +284 -13
  10. package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
  11. package/dist/frqtl/objects/instruments/capture.js +1 -24
  12. package/dist/frqtl/objects/instruments/hosted-eyes.js +216 -29
  13. package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
  14. package/dist/frqtl/objects/record/Record.js +346 -0
  15. package/dist/frqtl/objects/record/open-record.js +22 -0
  16. package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
  17. package/dist/frqtl/objects/schema/scopes.js +2 -2
  18. package/dist/frqtl/objects/setup/_worldInit.js +1 -2
  19. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
  20. package/dist/frqtl/parts/CameraControls.js +8 -0
  21. package/dist/frqtl/parts/DetectorWall.js +1 -1
  22. package/dist/frqtl/parts/FrqtlVision.js +21 -6
  23. package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
  24. package/dist/frqtl/parts/LargeObject.js +2 -2
  25. package/dist/frqtl/parts/LiveChart.js +2 -2
  26. package/dist/frqtl/parts/LogicGate.js +3 -3
  27. package/dist/frqtl/parts/OvenBox.js +1 -1
  28. package/dist/frqtl/parts/PointSource.js +1 -1
  29. package/dist/frqtl/parts/SlitLightSource.js +1 -1
  30. package/dist/frqtl/parts/SlitScreen.js +1 -1
  31. package/dist/frqtl/parts/SphereEmitter.js +12 -12
  32. package/dist/frqtl/parts/Trace.js +1 -1
  33. package/dist/frqtl/parts/Tunnel.js +11 -11
  34. package/dist/frqtl/parts/Wall.js +20 -10
  35. package/dist/frqtl/scenes/circuit-sat.js +1 -0
  36. package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
  37. package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
  38. package/dist/frqtl/scenes/mirror-route.js +96 -79
  39. package/dist/shim/bundle/index.js +118 -96
  40. package/dist/shim/bundle/indexHtml.js +66 -39
  41. package/dist/shim/controls/index.js +1 -1
  42. package/dist/shim/engine/artifact-shell.js +1 -1
  43. package/dist/shim/engine/router.js +3 -3
  44. package/dist/shim/host/host-view.js +228 -0
  45. package/dist/shim/host/index.js +18 -0
  46. package/dist/shim/index.js +3 -5
  47. package/dist/shim/notebook/format.js +1 -1
  48. package/dist/shim/notebook/kernel.js +1 -1
  49. package/dist/shim/notebook/setup-builder.js +2 -2
  50. package/dist/shim/notebook/store.js +2 -3
  51. package/dist/shim/package.json +1 -0
  52. package/dist/shim/page-params/index.js +2 -30
  53. package/dist/shim/project/fork.js +293 -0
  54. package/dist/shim/project/identity.js +3 -1
  55. package/dist/shim/project/index.js +16 -3
  56. package/dist/shim/project/link.js +224 -46
  57. package/dist/shim/project/manifest.js +151 -0
  58. package/dist/shim/project/parts-writer.js +68 -7
  59. package/dist/shim/project/read-text.js +43 -0
  60. package/dist/shim/project/record-format.js +345 -0
  61. package/dist/shim/project/specifiers.js +104 -15
  62. package/dist/shim/registry/exhibits.js +15 -34
  63. package/dist/shim/registry/index.js +3 -3
  64. package/dist/shim/registry/scene-roster.js +35 -0
  65. package/dist/shim/registry/scene-source.js +48 -64
  66. package/dist/shim/registry/walkthrough.js +6 -6
  67. package/dist/shim/render-state/apply.js +66 -31
  68. package/dist/shim/render-state/index.js +6 -2
  69. package/dist/shim/runtime/index.js +4 -4
  70. package/dist/shim/storage/codec-chains.js +88 -0
  71. package/dist/shim/storage/folders.js +205 -0
  72. package/dist/shim/storage/index.js +64 -8
  73. package/dist/shim/storage/make-namespace-store.js +5 -5
  74. package/llms.txt +4 -3
  75. package/package.json +19 -1
  76. package/dist/shim/bundle/frqnb.js +0 -112
  77. package/dist/shim/bundle/manifest.js +0 -95
  78. package/dist/shim/registry/experiments.js +0 -215
  79. package/dist/shim/storage/modules.js +0 -249
  80. package/dist/shim/storage/projects.js +0 -150
@@ -82,7 +82,7 @@ import { V1_REPLACEMENTS } from '../objects/v1-replacements.js';
82
82
  var zDepth = (depth !== null) ? depth : null;
83
83
 
84
84
  this.universe = universe;
85
- this._schemaKind = 'SlitLightSource';
85
+ this._schemaKind = this.constructor.name;
86
86
 
87
87
  // Light source diameter: hardcoded to 16 matching devsdk's pointSOURCESIZE.
88
88
  // (wavelength * 0.25 * 2 = 32 would double the emission radius and velocity)
@@ -137,7 +137,7 @@ import { ObjectSchema } from '../objects/schema/index.js';
137
137
  */
138
138
  function SlitScreen(universe, wallLeft, wallWidth, canvasHeight, wavelength, absorbAtSlitWall, detectAtSlits, slitHeight, gapHeight, depth, holeDepth, linerScatter) {
139
139
  this.universe = universe;
140
- this._schemaKind = 'SlitScreen';
140
+ this._schemaKind = this.constructor.name;
141
141
  this.wavelength = wavelength;
142
142
  this.depth = (depth !== undefined && depth !== null) ? depth : null;
143
143
  // 'current' (default/omitted) = today's exact liner behavior, byte-identical.
@@ -157,7 +157,7 @@ import { CollisionTypes } from './CollisionTypes.js';
157
157
  * @param {Object} [opts]
158
158
  */
159
159
  function SphereEmitter(position, opts) {
160
- this._schemaKind = 'SphereEmitter';
160
+ this._schemaKind = this.constructor.name;
161
161
  // First-arg overload: an array is a position; a plain object is opts
162
162
  // (position defaults to canvas center → byte-identical factory default).
163
163
  if (Array.isArray(position)) {
@@ -180,7 +180,7 @@ import { CollisionTypes } from './CollisionTypes.js';
180
180
  // self-throttle target — the emitter deactivates when the live population
181
181
  // measured by the counting Wall NAMED by `gateCounter` reaches the cap, and
182
182
  // reactivates below it. Compiled at CONSTRUCTION into a host tick-fn (see
183
- // _installEmissionGate in addTo) so the gate is codegen-round-trippable and
183
+ // _installEmissionGate in addTo) so the gate is captured by the parts writer and
184
184
  // Restart-safe (the Restart-safe host-tick-fn rule — the gate is a
185
185
  // construction side effect of captured descriptor fields, exactly like the
186
186
  // ABSORB radiator, NEVER free recipe JS). Validation is throw-or-apply:
@@ -275,7 +275,7 @@ import { CollisionTypes } from './CollisionTypes.js';
275
275
  * `fn(universe)` seam — the SAME seam as the emit tick-fn, the blackbody
276
276
  * thermostat, and the ABSORB radiator). Each tick it:
277
277
  * 1. LAZILY resolves the counting Wall by NAME on the FIRST tick (handles are
278
- * registration-fresh and never survive codegen — the name is the reference;
278
+ * registration-fresh and never survive a rebuild — the name is the reference;
279
279
  * lazy resolution makes emitter/counter construction order irrelevant).
280
280
  * Unresolved after the first tick → THROW (never a silent uncapped run).
281
281
  * 2. Reads the live population through `counter.getLiveCount(universe._lastTickData)`
@@ -447,7 +447,7 @@ import { CollisionTypes } from './CollisionTypes.js';
447
447
  // captureProps(sphereObj, SphereEmitterDescriptor) reads `sphereObj[field.name]`
448
448
  // DIRECTLY for every descriptor field (a getter satisfies it). These expose the
449
449
  // LIVE resolved value (from the factory's control) so a dragged/edited emitter
450
- // Saves at its current state — the codegen round-trip authority. Boot-level fields
450
+ // Saves at its current state — what the parts writer captures. Boot-level fields
451
451
  // (fundamentalWaveLength) that the control doesn't surface read from the
452
452
  // construction opts (they cannot change live). `rect` is a WRITE-ONLY reposition
453
453
  // adapter: its getter is deliberately `undefined` so captureProps SKIPS it —
@@ -463,7 +463,7 @@ import { CollisionTypes } from './CollisionTypes.js';
463
463
  // the ObjectFactory marker diameter + makeControlsForObject's rect-seed. captureProps no
464
464
  // longer trips on this: the descriptor's rect field is `writeOnly:true`, so captureProps
465
465
  // SKIPS it by the descriptor flag (not by the getter being undefined) — `center` stays the
466
- // single captured position authority (codegen round-trip unchanged). The write path
466
+ // single captured position authority. The write path
467
467
  // (rect → setCenter) is still the applyProp adapter (updateObjectProperty).
468
468
  get: function () {
469
469
  var c = (this._control && typeof this._control.getCenter === 'function')
@@ -487,7 +487,7 @@ import { CollisionTypes } from './CollisionTypes.js';
487
487
  Object.defineProperty(SphereEmitter.prototype, 'is2D', _liveGetter(function (c) { return c.getIs2D(); }));
488
488
  Object.defineProperty(SphereEmitter.prototype, 'orientationDeg', _liveGetter(function (c) { return c.getOrientation(); }));
489
489
  Object.defineProperty(SphereEmitter.prototype, 'emissionBox', _liveGetter(function (c) { return c.getEmissionBox(); }));
490
- // Declared emission-gate fields (captured for codegen round-trip; undefined
490
+ // Declared emission-gate fields (captured by the parts writer; undefined
491
491
  // when uncapped → captureProps SKIPS them → byte-identical legacy scenes).
492
492
  Object.defineProperty(SphereEmitter.prototype, 'emissionCap', {
493
493
  get: function () { return this._emissionCap; }
@@ -585,7 +585,7 @@ import { CollisionTypes } from './CollisionTypes.js';
585
585
  * builder composes a LargeObject-visual marker + the factory's host emit
586
586
  * tick-fn (SU1 — the visual LO is NONE-collision, non-leaf, zero physics).
587
587
  *
588
- * This descriptor is the SINGLE field authority: captureProps (codegen),
588
+ * This descriptor is the SINGLE field authority: captureProps (the parts writer),
589
589
  * makeControlsForObject (the Inspector), and FrqtlApp.updateObjectProperty
590
590
  * (the live-edit write path) ALL drive off it — no hand-list anywhere.
591
591
  *
@@ -600,7 +600,7 @@ import { CollisionTypes } from './CollisionTypes.js';
600
600
  * round-trip; the Inspector just renders it read-only.
601
601
  *
602
602
  * FIELD SET is COMPLETE w.r.t. every scene-affecting setupSphereEmitter3D opt
603
- * (a missing opt = lossy codegen). `emissionBoxSide` is subsumed by the
603
+ * (a missing opt = a lossy capture). `emissionBoxSide` is subsumed by the
604
604
  * captured derived `emissionBox`. There is no init flag: the SKETCH inits the
605
605
  * world once (setupSphereEmitter3D.initWorld / the co-init call), so a second emitter on
606
606
  * one universe (6A/6B/6C) is simply a second object.
@@ -635,7 +635,7 @@ import { CollisionTypes } from './CollisionTypes.js';
635
635
  live: true,
636
636
  // PR-1 item iii: the facade's `.rect` getter now returns a REAL rect DERIVED from
637
637
  // the live center (so computeEntityBboxForDesign reads fresh — no stale bbox). To
638
- // keep `center` the SINGLE captured position authority (codegen round-trip), this
638
+ // keep `center` the SINGLE captured position authority, this
639
639
  // field is `writeOnly:true` → captureProps SKIPS it BY THE FLAG (not by the getter
640
640
  // being undefined). The WRITE path is unchanged: a rect write maps rect-CENTER →
641
641
  // the emission center (control.setCenter, via applyProp).
@@ -653,7 +653,7 @@ import { CollisionTypes } from './CollisionTypes.js';
653
653
  engineVisible: false,
654
654
  live: false,
655
655
  description: 'Emission SOURCE POSITION in absolute canvas px as [x,y,z] (the captured ' +
656
- 'position authority — codegen round-trips this, not rect). Edited live via the rect ' +
656
+ 'position authority — the parts writer writes this, not rect). Edited live via the rect ' +
657
657
  'adapter / the visual marker drag; shown read-only here.'
658
658
  },
659
659
  {
@@ -698,7 +698,7 @@ import { CollisionTypes } from './CollisionTypes.js';
698
698
  // pattern; circulation-emission-gate-contract §2 field shapes).
699
699
  // Sensor = a placeable counting Wall (name in gateCounter); regulation =
700
700
  // emitter-side sugar compiled at CONSTRUCTION into a host tick-fn (the
701
- // Restart-safe host-tick-fn rule — codegen captures these two fields and
701
+ // Restart-safe host-tick-fn rule — the parts writer captures these two fields and
702
702
  // the reconstructed emitter's constructor rebuilds the gate).
703
703
  name: 'emissionCap',
704
704
  type: 'number',
@@ -811,7 +811,7 @@ import { CollisionTypes } from './CollisionTypes.js';
811
811
  constraint: { min: 0, integer: true },
812
812
  engineVisible: false,
813
813
  live: true,
814
- // Field NAME stays `leafDimension` (codegen/capture key; a rename = round-trip
814
+ // Field NAME stays `leafDimension` (the capture key; a rename = round-trip
815
815
  // break + a second name = schema mirror — Q-A2). The Inspector shows
816
816
  // the display LABEL "Emit Dimension"; the write routes through applyProp →
817
817
  // control.setLeafDimension (host-side addDimension + closure var flip; the NEXT
@@ -245,7 +245,7 @@ import { Wall as _Wall } from './Wall.js';
245
245
  function Trace(universe, opts) {
246
246
  opts = opts || {};
247
247
  this.universe = universe;
248
- this._schemaKind = 'Trace';
248
+ this._schemaKind = this.constructor.name;
249
249
 
250
250
  var path = opts.path;
251
251
  if (!Array.isArray(path) || path.length < 2) {
@@ -239,7 +239,7 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
239
239
  function Tunnel(universe, opts) {
240
240
  opts = opts || {};
241
241
  this.universe = universe;
242
- this._schemaKind = 'Tunnel';
242
+ this._schemaKind = this.constructor.name;
243
243
 
244
244
  var left = opts.left, top = opts.top;
245
245
  var length = opts.length, bore = opts.bore;
@@ -307,7 +307,7 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
307
307
  'contention rather than the point source. (FAIL-LOUD.)');
308
308
  }
309
309
 
310
- // Stash the authored construction values (the codegen round-trip authority —
310
+ // Stash the authored construction values (what the parts writer captures —
311
311
  // the descriptor fields read these back verbatim, Box precedent).
312
312
  this.left = left; this.top = top; this.length = length; this.bore = bore;
313
313
  this.wallThickness = wallThickness; this.axis = axis; this.color = color;
@@ -316,7 +316,7 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
316
316
  // knob, so the Inspector must render its selector on every tunnel, which needs a
317
317
  // defined value to read. The three directed-only tunables follow the z/depth
318
318
  // OPT-IN precedent instead — left undefined in mirror mode so captureProps skips
319
- // them and a mirror tunnel's codegen gains exactly ONE key, not four.
319
+ // them and a mirror tunnel's captured entry gains exactly ONE key, not four.
320
320
  this.mode = mode;
321
321
  if (directed) {
322
322
  this.directedAngle = directedAngleDeg;
@@ -331,9 +331,9 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
331
331
  this.exitPhaseReset = exitPhaseReset;
332
332
  }
333
333
  // z/depth are DECLARED descriptor fields (TunnelDescriptor) so a 3D corridor
334
- // round-trips through codegen/Restart. A FLAT (2D) tunnel leaves them
335
- // `undefined` — captureProps skips undefined, so a 2D codegen stays
336
- // byte-identical to the pre-3D output (no `"z":0,"depth":null` noise).
334
+ // round-trips through the parts writer. A FLAT (2D) tunnel leaves them
335
+ // `undefined` — captureProps skips undefined, so a 2D capture carries
336
+ // no `"z":0,"depth":null` noise.
337
337
  if (hasDepth) { this.z = z; this.depth = depth; }
338
338
 
339
339
  // ── Mirror wall rects (two parallel plates a `bore` apart) ───────────
@@ -734,9 +734,9 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
734
734
  * Subscribe this tunnel's redirect to a runner's per-tick fan-out
735
735
  * (`FrqtlApp.addTickListener` — the multi-subscriber peer of the page's single
736
736
  * `onTick` slot, so it never steals it). Called by the ObjectFactory builder, which is
737
- * what makes the directed mode survive codegen → Restart: the mode is a DECLARED
738
- * descriptor field on the facade, so the rebuilt object re-wires itself. A raw tick-fn
739
- * hand-written in a recipe would NOT survive (Restart evals the codegen'd module).
737
+ * what makes the directed mode part of the declared object: the mode is a DECLARED
738
+ * descriptor field on the facade, so every build of it (a sketch entry, a Restart)
739
+ * re-wires itself.
740
740
  *
741
741
  * Idempotent, and a no-op for a mirror tunnel or a runner without the fan-out.
742
742
  * @param {Object} runner - an FrqtlApp (needs `addTickListener`).
@@ -1045,8 +1045,8 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
1045
1045
  // mode:'directed' — subscribe the host redirect to the runner's per-tick fan-out.
1046
1046
  // Self-gating + idempotent, so this is called unconditionally (a mirror tunnel
1047
1047
  // no-ops). Wiring it HERE, off the DECLARED `mode` field, is what makes the
1048
- // directed behavior survive codegen → Restart: the rebuilt facade re-subscribes
1049
- // itself, where a recipe-authored tick-fn would be lost.
1048
+ // directed behavior part of the declared object: every build of the facade
1049
+ // re-subscribes itself.
1050
1050
  if (typeof tunnel.attachToRunner === 'function') { tunnel.attachToRunner(ctx.runner); }
1051
1051
  return tunnel;
1052
1052
  }
@@ -178,10 +178,20 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
178
178
  }
179
179
 
180
180
  function _rectFromOpts(opts) {
181
- // Normalize an OBJECT-bag rect ({left,top,w,h} / {left,top,z,w,h,d} — the shape a
182
- // codegen round-trip captures from the live LO, restart-bug fix 2026-07-25) to the
183
- // canonical ARRAY form FIRST, so the 3D checks below (_rectIs3D/_rectDepth are
184
- // Array-based) see a captured 3D wall as 3D. Mirrors LargeObject's own rectToArray.
181
+ // ONE spelling of the z-slab: a rect that already carries z/depth (the 6-element form, or an
182
+ // object rect with z/d) beside the z / d / depth props would be two statements of one fact,
183
+ // one of them silently ignored. Refused, naming both.
184
+ var slabProps = ['z', 'd', 'depth'].filter(function (k) { return opts[k] !== undefined; });
185
+ if (slabProps.length && opts.rect && (Array.isArray(opts.rect) ? opts.rect.length === 6
186
+ : (opts.rect.z !== undefined || opts.rect.d !== undefined))) {
187
+ throw new Error('[Wall] rect is already the z/depth form ' + ValueNormalizer.rectTupleText(6) +
188
+ ' and the props also give ' + slabProps.join(', ') + ' — write the z-slab ONE way: the 6-element rect, ' +
189
+ 'or a 4-element rect beside z/depth. (FAIL-LOUD.)');
190
+ }
191
+ // Normalize an OBJECT-bag rect ({left,top,w,h} / {left,top,z,w,h,d} — the shape a live LO's
192
+ // rect reads as) to the canonical ARRAY form FIRST, so the 3D checks below
193
+ // (_rectIs3D/_rectDepth are Array-based) see such a 3D wall as 3D. Mirrors LargeObject's
194
+ // own rectToArray.
185
195
  if (opts.rect && !Array.isArray(opts.rect) && opts.rect.left !== undefined && opts.rect.w !== undefined) {
186
196
  var ob = opts.rect;
187
197
  var arr = (ob.z !== undefined || ob.d !== undefined)
@@ -382,7 +392,7 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
382
392
  self._radiationTickFn = tickFn;
383
393
  // The radiator OWNS the live emissivity (including the default when none was
384
394
  // stated). Read the facade prop back off it rather than leaving it undefined —
385
- // captureProps/codegen and the Inspector read `wall.emissivity`, and a facade that
395
+ // captureProps (the parts writer) and the Inspector read `wall.emissivity`, and a facade that
386
396
  // says nothing while the radiator says 0.5 is a second, wrong answer.
387
397
  self.emissivity = tickFn.emissivity();
388
398
  if (universe && Array.isArray(universe.experimentTickFunctions)) {
@@ -399,10 +409,10 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
399
409
  function Wall(universe, opts) {
400
410
  opts = opts || {};
401
411
  this.universe = universe;
402
- this._schemaKind = 'Wall';
412
+ this._schemaKind = this.constructor.name;
403
413
 
404
- // A `preset` expands to the v2 descriptor fields (construction sugar; codegen
405
- // round-trips the EXPANDED fields, never the preset name).
414
+ // A `preset` expands to the v2 descriptor fields (construction sugar; the parts
415
+ // writer captures the EXPANDED fields, never the preset name).
406
416
  if (opts.preset) {
407
417
  var expanded = expandWallPreset(opts.preset);
408
418
  var merged = {}; Object.keys(opts).forEach(function (k) { if (k !== 'preset') { merged[k] = opts[k]; } });
@@ -437,7 +447,7 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
437
447
  // ── Descriptor-field read surface (captureProps / Inspector) ─────────────
438
448
  // captureProps(wall, WallDescriptor) reads wall[field.name] directly; these
439
449
  // getters expose the LIVE inner-LO state so a dragged/edited wall Saves at its
440
- // current state (the codegen round-trip authority). `rect` is the single
450
+ // current state (what the parts writer captures). `rect` is the single
441
451
  // captured position authority (the LO's own Rectangle, whose toJSON pins the
442
452
  // {left,top,w,h} wire shape).
443
453
  Object.defineProperty(Wall.prototype, 'rect', { get: function () { return this.lo ? this.lo.rect : undefined; } });
@@ -573,7 +583,7 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
573
583
  */
574
584
  Wall.prototype.setV2Prop = function (prop, value) {
575
585
  // EMISSIVITY — live radiating efficiency. The running radiator OWNS the value (one
576
- // live copy); the facade prop is a ferried mirror for the Inspector/codegen read.
586
+ // live copy); the facade prop is a ferried mirror for the Inspector and capture read.
577
587
  // Validation (fullness fraction in (0,1]) belongs to the owner and throws from there.
578
588
  if (prop === 'emissivity') {
579
589
  if (this.collisionBehavior !== 'absorb' || !this._radiationTickFn) {
@@ -22,6 +22,7 @@
22
22
  import { FrqtlControlOverlay, FrqtlControlsPanel, LogicGate, SphereEmitter, Trace } from '@descix/frqtl-web/parts';
23
23
  import * as ControlRegistry from '@descix/frqtl-web/shim/controls/registry.js';
24
24
 
25
+ // Code-declared on purpose: the emitter centres and rail traces derive from the gates' port geometry and RAIL (a knob rewrites it), and a Trace has no kind — a literal block would restate derived geometry by hand.
25
26
  export const parts = {};
26
27
 
27
28
  export default function buildExperiment(exp) {
@@ -67,9 +67,10 @@
67
67
  // 2D ONLY, deliberately. There is no `modes` strip and no `exp.is3D` guard: these
68
68
  // rungs declare one world each. A second dimensional variant would be a separate
69
69
  // recipe declaration, not a mode.
70
- // THE PAIR. `dse-two-emitters.js` is the SAME apparatus with NSRC = 2. The two files differ ONLY in
71
- // the title lines and the `var NSRC` line — lib/frqtl/scenes/test/scene-files.test.mjs
72
- // fails if any other line diverges, so the A/B the pair exists to make stays an A/B.
70
+ // THE PAIR. `dse-two-emitters.js` is the SAME apparatus with TWO source entries. The two files differ ONLY in the
71
+ // title lines, this note, and their `source*` entries of `parts` (with the world call naming the first
72
+ // of them) — lib/frqtl/scenes/test/scene-files.test.mjs fails if anything else diverges, so the A/B the
73
+ // pair exists to make stays an A/B.
73
74
 
74
75
  import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
75
76
 
@@ -116,7 +117,34 @@ const detW = Math.round(lamPx / 64); // 5 — plate thickness
116
117
  // the photoelectric experiment, and it saturates.
117
118
  const detectorPreset = 'transparent-probe';
118
119
 
120
+ const lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
121
+ const fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
122
+ const wlScale = Math.floor(fwl / 4); // 100
123
+ const cy = Math.round(H / 2); // 200 — the source plane's midline
124
+ // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
125
+ // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
126
+ // the strongest case. At this leg it also puts a band of EACH source on the
127
+ // midline y = H/2 (source 1's lower band and source 2's upper band coincide
128
+ // there) — the doubly-reinforced central peak, equidistant from both emitters,
129
+ // exactly where the one-source rung has no peak at all.
130
+ const sep = lamPx / 2; // 160 -> sources at y 120 and 280
131
+ // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
132
+ // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
133
+ // ball-rejected sampler as the omni source with the forward axis' range halved,
134
+ // so nothing about the walk changes, it is simply pointed at the plate. The
135
+ // default emission box is 0 for this preset, so the source is a single PIXEL —
136
+ // load-bearing, because any spread at birth blurs every band by that spread.
137
+ // NO START WALL: the backward population is empty by construction, and `wrap` is
138
+ // false, so the canvas edge kills whatever transverse wiggle out-runs its
139
+ // forward component. One sink, not two.
140
+ const SRC = {
141
+ preset: 'hemisphere', is2D: true, chargeMode: 'random',
142
+ wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
143
+ burstSize: 8, emitEveryTicks: 1
144
+ };
145
+
119
146
  export const parts = {
147
+ source: [SphereEmitter, { ...SRC, center: [srcX, cy] }],
120
148
  // FAR-EDGE SINK. The plate below counts WITHOUT removing, so quanta fly on past
121
149
  // it; this is the experiment-boundary sink downstream of the plate. It is not
122
150
  // load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
@@ -134,41 +162,11 @@ export const parts = {
134
162
  };
135
163
 
136
164
  export default function buildExperiment(exp) {
137
- var NSRC = 1;
138
- var lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
139
- var fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
140
- var wlScale = Math.floor(fwl / 4); // 100
141
165
  if (Math.abs(lamArg * wlScale - lamPx) > 1e-9) {
142
166
  throw new Error('[dse-h1] lamPx ' + lamPx + ' is not realisable as lambda_eff: the engine ' +
143
167
  'can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
144
168
  }
145
- var cy = Math.round(H / 2); // 200 — the source plane's midline
146
- // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
147
- // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
148
- // the strongest case. At this leg it also puts a band of EACH source on the
149
- // midline y = H/2 (source 1's lower band and source 2's upper band coincide
150
- // there) — the doubly-reinforced central peak, equidistant from both emitters,
151
- // exactly where the one-source rung has no peak at all.
152
- var sep = lamPx / 2; // 160 -> sources at y 120 and 280
153
- var srcYs = (NSRC === 2) ? [cy - sep / 2, cy + sep / 2] : [cy];
154
- // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
155
- // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
156
- // ball-rejected sampler as the omni source with the forward axis' range halved,
157
- // so nothing about the walk changes, it is simply pointed at the plate. The
158
- // default emission box is 0 for this preset, so the source is a single PIXEL —
159
- // load-bearing, because any spread at birth blurs every band by that spread.
160
- // NO START WALL: the backward population is empty by construction, and `wrap` is
161
- // false, so the canvas edge kills whatever transverse wiggle out-runs its
162
- // forward component. One sink, not two.
163
- var SRC = {
164
- preset: 'hemisphere', is2D: true, chargeMode: 'random',
165
- wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
166
- burstSize: 8, emitEveryTicks: 1
167
- };
168
- exp.createCanvas(CW, H, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
169
- for (var si = 0; si < srcYs.length; si++) {
170
- exp.addObject('SphereEmitter', Object.assign({}, SRC, { center: [srcX, Math.round(srcYs[si])] }));
171
- }
169
+ exp.createCanvas(CW, H, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
172
170
  const p = exp.declare(parts);
173
171
  // ── THE INSTRUMENT — the live count-by-y histogram, REGISTERED TO THE PLATE ──
174
172
  // The panel takes the plate's OWN rect y-origin (0) and height (H), so the two
@@ -67,9 +67,10 @@
67
67
  // 2D ONLY, deliberately. There is no `modes` strip and no `exp.is3D` guard: these
68
68
  // rungs declare one world each. A second dimensional variant would be a separate
69
69
  // recipe declaration, not a mode.
70
- // THE PAIR. `dse-one-emitter.js` is the SAME apparatus with NSRC = 1. The two files differ ONLY in
71
- // the title lines and the `var NSRC` line — lib/frqtl/scenes/test/scene-files.test.mjs
72
- // fails if any other line diverges, so the A/B the pair exists to make stays an A/B.
70
+ // THE PAIR. `dse-one-emitter.js` is the SAME apparatus with ONE source entry. The two files differ ONLY in the
71
+ // title lines, this note, and their `source*` entries of `parts` (with the world call naming the first
72
+ // of them) — lib/frqtl/scenes/test/scene-files.test.mjs fails if anything else diverges, so the A/B the
73
+ // pair exists to make stays an A/B.
73
74
 
74
75
  import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
75
76
 
@@ -116,7 +117,35 @@ const detW = Math.round(lamPx / 64); // 5 — plate thickness
116
117
  // the photoelectric experiment, and it saturates.
117
118
  const detectorPreset = 'transparent-probe';
118
119
 
120
+ const lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
121
+ const fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
122
+ const wlScale = Math.floor(fwl / 4); // 100
123
+ const cy = Math.round(H / 2); // 200 — the source plane's midline
124
+ // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
125
+ // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
126
+ // the strongest case. At this leg it also puts a band of EACH source on the
127
+ // midline y = H/2 (source 1's lower band and source 2's upper band coincide
128
+ // there) — the doubly-reinforced central peak, equidistant from both emitters,
129
+ // exactly where the one-source rung has no peak at all.
130
+ const sep = lamPx / 2; // 160 -> sources at y 120 and 280
131
+ // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
132
+ // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
133
+ // ball-rejected sampler as the omni source with the forward axis' range halved,
134
+ // so nothing about the walk changes, it is simply pointed at the plate. The
135
+ // default emission box is 0 for this preset, so the source is a single PIXEL —
136
+ // load-bearing, because any spread at birth blurs every band by that spread.
137
+ // NO START WALL: the backward population is empty by construction, and `wrap` is
138
+ // false, so the canvas edge kills whatever transverse wiggle out-runs its
139
+ // forward component. One sink, not two.
140
+ const SRC = {
141
+ preset: 'hemisphere', is2D: true, chargeMode: 'random',
142
+ wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
143
+ burstSize: 8, emitEveryTicks: 1
144
+ };
145
+
119
146
  export const parts = {
147
+ source1: [SphereEmitter, { ...SRC, center: [srcX, Math.round(cy - sep / 2)] }],
148
+ source2: [SphereEmitter, { ...SRC, center: [srcX, Math.round(cy + sep / 2)] }],
120
149
  // FAR-EDGE SINK. The plate below counts WITHOUT removing, so quanta fly on past
121
150
  // it; this is the experiment-boundary sink downstream of the plate. It is not
122
151
  // load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
@@ -134,41 +163,11 @@ export const parts = {
134
163
  };
135
164
 
136
165
  export default function buildExperiment(exp) {
137
- var NSRC = 2;
138
- var lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
139
- var fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
140
- var wlScale = Math.floor(fwl / 4); // 100
141
166
  if (Math.abs(lamArg * wlScale - lamPx) > 1e-9) {
142
167
  throw new Error('[dse-h1] lamPx ' + lamPx + ' is not realisable as lambda_eff: the engine ' +
143
168
  'can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
144
169
  }
145
- var cy = Math.round(H / 2); // 200 — the source plane's midline
146
- // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
147
- // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
148
- // the strongest case. At this leg it also puts a band of EACH source on the
149
- // midline y = H/2 (source 1's lower band and source 2's upper band coincide
150
- // there) — the doubly-reinforced central peak, equidistant from both emitters,
151
- // exactly where the one-source rung has no peak at all.
152
- var sep = lamPx / 2; // 160 -> sources at y 120 and 280
153
- var srcYs = (NSRC === 2) ? [cy - sep / 2, cy + sep / 2] : [cy];
154
- // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
155
- // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
156
- // ball-rejected sampler as the omni source with the forward axis' range halved,
157
- // so nothing about the walk changes, it is simply pointed at the plate. The
158
- // default emission box is 0 for this preset, so the source is a single PIXEL —
159
- // load-bearing, because any spread at birth blurs every band by that spread.
160
- // NO START WALL: the backward population is empty by construction, and `wrap` is
161
- // false, so the canvas edge kills whatever transverse wiggle out-runs its
162
- // forward component. One sink, not two.
163
- var SRC = {
164
- preset: 'hemisphere', is2D: true, chargeMode: 'random',
165
- wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
166
- burstSize: 8, emitEveryTicks: 1
167
- };
168
- exp.createCanvas(CW, H, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
169
- for (var si = 0; si < srcYs.length; si++) {
170
- exp.addObject('SphereEmitter', Object.assign({}, SRC, { center: [srcX, Math.round(srcYs[si])] }));
171
- }
170
+ exp.createCanvas(CW, H, SphereEmitter.worldCfg(parts.source1[1])); // THE WORLD: a 2D canvas + universe — first, then parts
172
171
  const p = exp.declare(parts);
173
172
  // ── THE INSTRUMENT — the live count-by-y histogram, REGISTERED TO THE PLATE ──
174
173
  // The panel takes the plate's OWN rect y-origin (0) and height (H), so the two