@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.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 +87 -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 +2501 -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 +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -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 +259 -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 +2474 -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 +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -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 +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -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 +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -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 +310 -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 +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -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 +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -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 +280 -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 +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -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 +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -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 +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -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 +244 -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 +212 -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 +692 -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 +326 -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 +672 -0
- package/dist/shim/notebook/knob.js +151 -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 +510 -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 +214 -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 +807 -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 +62 -4
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sustainedSource — the ONE hand-placed-quantum source surface (ws-dse-3d, CEO
|
|
5
|
+
* ruling 2026-07-14: "Tunnel Lab and LightSource Lab share ONE source surface
|
|
6
|
+
* and differ only in declared composition"). REPLACES two independently
|
|
7
|
+
* hand-rolled injector tick-fns (`setupTunnelLab.js`'s and
|
|
8
|
+
* `setupLightSourceLab.js`'s own, admitted by the latter's own comment to
|
|
9
|
+
* "mirror setupTunnelLab.js byte-for-byte") with ONE authority that both labs,
|
|
10
|
+
* and `LightSource` itself (via its opt-in `options.sustain`), consume.
|
|
11
|
+
*
|
|
12
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* `SOURCE_SPEC_FIELDS` is the declared contract for what a source stamps on
|
|
15
|
+
* every hand-placed quantum, plus its burst schedule — the schema-mirror
|
|
16
|
+
* discipline (root CLAUDE.md "Engineering Culture"): the owner (this file)
|
|
17
|
+
* exports the field list + defaults; every consumer calls `normalizeSourceSpec`
|
|
18
|
+
* to get a FULL bag (never hand-re-enumerates a subset), so a field omission is
|
|
19
|
+
* structurally impossible to silently diverge on (the exact bug this bite
|
|
20
|
+
* fixes: one injector stamped `wavelength`, the other didn't, and nothing
|
|
21
|
+
* forced them to agree). `sustained-source.test.mjs` is driven off this exact
|
|
22
|
+
* exported list — adding a field here without a matching test entry is a CI
|
|
23
|
+
* failure, not a silent gap.
|
|
24
|
+
*
|
|
25
|
+
* `placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, spec, count)`
|
|
26
|
+
* is the single low-level placement primitive — computes the entrance pixel
|
|
27
|
+
* from `innerRect`+`capNegX`+`spec.entranceBias` (the SAME formula
|
|
28
|
+
* `LightSource.js`'s own one-shot burst and both labs' old hand-rolled
|
|
29
|
+
* injectors independently derived), draws a velocity per `spec.velocityProfile`,
|
|
30
|
+
* and calls `makeLeafFrame`/`injectFrame`/`setFrameProperty` — the SAME
|
|
31
|
+
* generic primitives every hand-placed-quantum experiment already uses (G-EMIT-2,
|
|
32
|
+
* `docs/internal/engine/ENGINE_BOUNDARY_MODEL.md`). Used for BOTH a one-shot burst (LightSource's
|
|
33
|
+
* own construction-time placement) and a recurring burst (`buildSustainedSource`'s
|
|
34
|
+
* tick-fn) — "the one injector then stamps quantum properties ... in exactly ONE
|
|
35
|
+
* place" (the architect's own worked-example blueprint, Section 4 item 1).
|
|
36
|
+
*
|
|
37
|
+
* `buildSustainedSource(universe, innerRect, capNegX, hasRealDepth, specPartial)`
|
|
38
|
+
* builds+registers a recurring Phase-A host tick-fn (mirrors `emitterProgram.js`'s
|
|
39
|
+
* own registration point — a pre-tick primitive with no Phase-D timing
|
|
40
|
+
* constraint, unlike `directedRedirectProgram.js`'s option1 redirect) that calls
|
|
41
|
+
* `placeSourceQuanta` every `sourceState.rate` ticks. Returns `{sourceState,
|
|
42
|
+
* dispose}` — `sourceState` is the LIVE, mutable normalized spec (a UI slider
|
|
43
|
+
* writes `sourceState.wavelength = v` etc.; the tick-fn reads it fresh every
|
|
44
|
+
* call — no re-registration, no reconstruction, matching the established
|
|
45
|
+
* `setupTunnelLab.js`/`setupLightSourceLab.js` LIVE-knob convention exactly).
|
|
46
|
+
* `dispose()` tags-then-splices the tick-fn out of `experimentTickFunctions` by
|
|
47
|
+
* identity (`injectorTickFn._sourceInjector`) — the SAME idiom
|
|
48
|
+
* `QuantumEmitter.prototype.dispose` and every prior lab-level injector already
|
|
49
|
+
* used; see `FrqtlApp.prototype.setup()`'s own dispose-before-reinit
|
|
50
|
+
* convention (`reference_frqtlapp_setup_canonical_teardown.md`).
|
|
51
|
+
*
|
|
52
|
+
* @typedef {Object} SourceSpec
|
|
53
|
+
* @property {number|null} wavelength - Per-frame wavelength override (Engine
|
|
54
|
+
* Invariant #6 `mass = max(1, capacity/wavelength)`). `null` (default) =
|
|
55
|
+
* engine default (`mass = capacity`, i.e. fullness=1, top of the ROYGBIV
|
|
56
|
+
* ramp). THIS is the field whose divergence (one injector set it, the other
|
|
57
|
+
* didn't) produced the CEO's "different particle colors" report — see
|
|
58
|
+
* `docs/physics/COLOR_RENDER_LEARNINGS.md` and the ws-dse-3d investigation.
|
|
59
|
+
* @property {number|null} charge - Per-frame charge override (0=Positive,
|
|
60
|
+
* 1=Negative). `null` (default) = random draw per quantum.
|
|
61
|
+
* @property {'beam'|'spray'} velocityProfile - `'beam'` (default) = collimated
|
|
62
|
+
* single direction, `orientationRad`+`speed` (LightSource's own convention —
|
|
63
|
+
* `vy=vz=0` always, a deliberately directed/phase-locked stream). `'spray'` =
|
|
64
|
+
* a biased random fan (`spraySpec`) — `setupTunnelLab.js`'s own original
|
|
65
|
+
* uniformization-demo shape. Named, selectable, NEVER divergent hand-code
|
|
66
|
+
* (architect Section 3 finding E).
|
|
67
|
+
* @property {number} speed - `'beam'` profile only: speed magnitude.
|
|
68
|
+
* @property {number} orientationRad - `'beam'` profile only: direction,
|
|
69
|
+
* radians (`0` = +X).
|
|
70
|
+
* @property {{vxMin:number,vxRange:number,vyMin:number,vyRange:number}} spraySpec
|
|
71
|
+
* - `'spray'` profile only: `vx = vxMin + random()*vxRange`,
|
|
72
|
+
* `vy = vyMin + random()*vyRange` — `setupTunnelLab.js`'s ORIGINAL literal
|
|
73
|
+
* values (0.2/0.6/0.1/0.7), now named+declared instead of hardcoded inline.
|
|
74
|
+
* @property {'center'|'top'} entranceBias - `'center'` (default) = bore
|
|
75
|
+
* Y-centreline (`LightSource.js`'s own convention). `'top'` = bunched 2px
|
|
76
|
+
* from the TOP wall (`setupTunnelLab.js`'s original biased-entry shape — "a
|
|
77
|
+
* symmetric/spread entrance is already balanced and shows no washout effect
|
|
78
|
+
* at all").
|
|
79
|
+
* @property {boolean} zIsotropy - When real depth is armed AND
|
|
80
|
+
* `velocityProfile==='spray'`: draw `vz = (random()*2-1)*spraySpec.vyRange`
|
|
81
|
+
* (the SAME half-range as the vy spread) — the 3D-SYMMETRIC EMISSION fix
|
|
82
|
+
* (ws-dse-3d, theorist §4 option (a): "draw vz symmetric to the vy spread").
|
|
83
|
+
* `'beam'` profile is UNAFFECTED regardless of this flag (`vy=vz=0` stays —
|
|
84
|
+
* collimation is deliberate; theorist §4 option (b): the beam's isotropic
|
|
85
|
+
* TRANSVERSE WAVE already does the 3D work, measured y≈z). Default `true`.
|
|
86
|
+
* @property {number|null} level - `makeLeafFrame`'s `level` override (rarely
|
|
87
|
+
* used; `LightSource.js`'s own `options.particle.level`). `null` (default).
|
|
88
|
+
* @property {number|undefined} mass - post-placement `mass` override via
|
|
89
|
+
* `setFrameProperty` (`LightSource.js`'s own `options.particle.mass`).
|
|
90
|
+
* `undefined` (default) = no override.
|
|
91
|
+
* @property {number} density - Quanta hand-placed per burst.
|
|
92
|
+
* @property {number} rate - Ticks between bursts (an `everyNTicks` cadence).
|
|
93
|
+
* @property {boolean} active - Master on/off for the recurring source.
|
|
94
|
+
*
|
|
95
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
96
|
+
*
|
|
97
|
+
* Zero new physics (canonical-bench SU1): every write here is the SAME
|
|
98
|
+
* `makeLeafFrame`/`injectFrame`/`setFrameProperty`/`universe.random()` primitive
|
|
99
|
+
* chain every hand-placed-quantum experiment already uses — this module only
|
|
100
|
+
* CONSOLIDATES two independently-hand-rolled call sites into one, and adds a
|
|
101
|
+
* genuinely-new DECLARED knob (`zIsotropy`) that draws an EXTRA `vz` momentum
|
|
102
|
+
* component using the identical `(random()*2-1)*range` draw shape the `vy`
|
|
103
|
+
* spread already used — no new mechanism, just the same mechanism applied to a
|
|
104
|
+
* third axis. `velocityProfile:'spray'` + `zIsotropy:true` (tunnel-lab's new
|
|
105
|
+
* default) is expected to raise the settled z/y dispersion-spread ratio from
|
|
106
|
+
* the tester-measured 0.23 (pinned z, zero vz ever drawn) toward something
|
|
107
|
+
* close to the y-spread itself (the injected fan becomes 3D-symmetric; the
|
|
108
|
+
* existing free-walk + NEAR/FAR wall bounces already handle z correctly once
|
|
109
|
+
* they receive real z-momentum to work with — the measured `ax_only` branch
|
|
110
|
+
* gives y≈z isotropic transverse spread).
|
|
111
|
+
*/
|
|
112
|
+
'use strict';
|
|
113
|
+
|
|
114
|
+
// SOURCE_SPEC_FIELDS — the ONE declared contract (schema-mirror discipline,
|
|
115
|
+
// root CLAUDE.md). `default` is either a primitive or an object (deep-copied
|
|
116
|
+
// per-normalize call so no two sourceStates ever share a mutable spraySpec).
|
|
117
|
+
var SOURCE_SPEC_FIELDS = {
|
|
118
|
+
wavelength: { default: null, desc: 'Per-frame wavelength override (Engine Invariant #6 mass=max(1,capacity/wavelength)). null = engine default (mass=capacity).' },
|
|
119
|
+
charge: { default: null, desc: 'Per-frame charge override (0=Positive,1=Negative). null = random draw per quantum.' },
|
|
120
|
+
velocityProfile: { default: 'beam', enum: ['beam', 'spray'], desc: "'beam' = collimated single-direction (orientationRad+speed, vy=vz=0); 'spray' = biased random fan (spraySpec)." },
|
|
121
|
+
speed: { default: 1, desc: "'beam' profile only: speed magnitude." },
|
|
122
|
+
orientationRad: { default: 0, desc: "'beam' profile only: direction, radians (0 = +X)." },
|
|
123
|
+
spraySpec: { default: { vxMin: 0.2, vxRange: 0.6, vyMin: 0.1, vyRange: 0.7 }, desc: "'spray' profile only: vx=vxMin+random()*vxRange, vy=vyMin+random()*vyRange (setupTunnelLab.js's original literal fan)." },
|
|
124
|
+
entranceBias: { default: 'center', enum: ['center', 'top'], desc: "'center' = bore Y-centreline (LightSource.js's convention); 'top' = bunched 2px from the TOP wall (setupTunnelLab.js's original biased-entry/uniformization-demo shape)." },
|
|
125
|
+
zIsotropy: { default: true, desc: "When real depth is armed AND velocityProfile='spray': draw vz=(random()*2-1)*spraySpec.vyRange (SAME half-range as vy) for a 3D-symmetric fan. 'beam' stays vy=vz=0 regardless (deliberate collimation)." },
|
|
126
|
+
level: { default: null, desc: "makeLeafFrame's level override (rarely used)." },
|
|
127
|
+
mass: { default: undefined, desc: 'Post-placement mass override via setFrameProperty. undefined = no override.' },
|
|
128
|
+
density: { default: 5, desc: 'Quanta hand-placed per burst.' },
|
|
129
|
+
rate: { default: 10, desc: 'Ticks between bursts (everyNTicks cadence).' },
|
|
130
|
+
active: { default: true, desc: 'Master on/off for the recurring source.' },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
var SOURCE_SPEC_FIELD_NAMES = Object.keys(SOURCE_SPEC_FIELDS);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Normalize a partial spec into a FULL bag (every declared field present,
|
|
137
|
+
* defaults filled in) — the schema-mirror discipline's "consumers ferry the
|
|
138
|
+
* normalized bag" half. Never hand-re-enumerate a subset of
|
|
139
|
+
* SOURCE_SPEC_FIELDS elsewhere; always go through this function.
|
|
140
|
+
* @param {Object} [partial]
|
|
141
|
+
* @returns {SourceSpec}
|
|
142
|
+
*/
|
|
143
|
+
function normalizeSourceSpec(partial) {
|
|
144
|
+
partial = partial || {};
|
|
145
|
+
var out = {};
|
|
146
|
+
for (var i = 0; i < SOURCE_SPEC_FIELD_NAMES.length; i++) {
|
|
147
|
+
var key = SOURCE_SPEC_FIELD_NAMES[i];
|
|
148
|
+
if (partial[key] !== undefined) {
|
|
149
|
+
out[key] = (key === 'spraySpec')
|
|
150
|
+
? Object.assign({}, SOURCE_SPEC_FIELDS.spraySpec.default, partial[key]) // partial spraySpec override
|
|
151
|
+
: partial[key];
|
|
152
|
+
} else {
|
|
153
|
+
var def = SOURCE_SPEC_FIELDS[key].default;
|
|
154
|
+
out[key] = (def !== null && typeof def === 'object') ? Object.assign({}, def) : def; // fresh copy, never shared
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compute the fixed entrance pixel from a tunnel/tube's innerRect — the ONE
|
|
162
|
+
* formula `LightSource.js`'s own one-shot burst and both labs' original
|
|
163
|
+
* hand-rolled injectors independently derived (now declared once).
|
|
164
|
+
* @param {Object} innerRect - {left, top, z, w, h, d}
|
|
165
|
+
* @param {boolean} capNegX - true = closed breech (Gun-Chamber convention).
|
|
166
|
+
* @param {'center'|'top'} entranceBias
|
|
167
|
+
* @returns {{x:number, y:number}}
|
|
168
|
+
*/
|
|
169
|
+
function _computeEntrance(innerRect, capNegX, entranceBias) {
|
|
170
|
+
var x = capNegX ? (innerRect.left + 2) : (innerRect.left + innerRect.w / 2);
|
|
171
|
+
var y = (entranceBias === 'top') ? (innerRect.top + 2) : (innerRect.top + innerRect.h / 2);
|
|
172
|
+
return { x: x, y: y };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The single low-level placement primitive — hand-places `count` quanta at
|
|
177
|
+
* ONE shared entrance pixel (the I1 scale-sweep's own validated ensemble
|
|
178
|
+
* technique: injection-time overlap does not merge same-tick frames; each
|
|
179
|
+
* leaf's own move-gate/wavelength-oscillator RNG draws diverge it from its
|
|
180
|
+
* siblings over subsequent ticks). Used for BOTH a one-shot burst
|
|
181
|
+
* (`LightSource.js`'s construction) and a recurring burst
|
|
182
|
+
* (`buildSustainedSource`'s tick-fn) — ONE place quantum properties get
|
|
183
|
+
* stamped.
|
|
184
|
+
* @param {Object} universe
|
|
185
|
+
* @param {Object} innerRect
|
|
186
|
+
* @param {boolean} capNegX
|
|
187
|
+
* @param {boolean} hasRealDepth - gate on the CALLER's explicit request for
|
|
188
|
+
* z-containment (N1 fix precedent, `LightSource.js`) — NEVER derive this
|
|
189
|
+
* from `innerRect.d`, which is always a positive placeholder even when
|
|
190
|
+
* the tunnel is flat 2D-in-3D.
|
|
191
|
+
* @param {SourceSpec} spec - a NORMALIZED spec (pass through
|
|
192
|
+
* `normalizeSourceSpec` first — this function does not fill defaults).
|
|
193
|
+
* @param {number} count
|
|
194
|
+
* @returns {Array<{handle:*, qid:*}>}
|
|
195
|
+
*/
|
|
196
|
+
function placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, spec, count) {
|
|
197
|
+
var entrance = _computeEntrance(innerRect, capNegX, spec.entranceBias);
|
|
198
|
+
var entranceZ = hasRealDepth ? (innerRect.z + innerRect.d / 2) : null;
|
|
199
|
+
var placed = [];
|
|
200
|
+
for (var i = 0; i < count; i++) {
|
|
201
|
+
var vx, vy, vz;
|
|
202
|
+
if (spec.velocityProfile === 'spray') {
|
|
203
|
+
var s = spec.spraySpec;
|
|
204
|
+
vx = s.vxMin + universe.random() * s.vxRange;
|
|
205
|
+
vy = s.vyMin + universe.random() * s.vyRange;
|
|
206
|
+
// 3D-SYMMETRIC EMISSION (ws-dse-3d, theorist §4 option (a)): draw
|
|
207
|
+
// vz symmetric to the vy spread, SAME half-range, only when real
|
|
208
|
+
// depth is armed AND the caller opted into zIsotropy (default
|
|
209
|
+
// true) — makes the injected fan 3D-isotropic instead of pinning
|
|
210
|
+
// z-momentum at zero forever (the measured z/y=0.18-0.23 flatness
|
|
211
|
+
// root cause — architect Section 2e, tester Section C).
|
|
212
|
+
vz = (hasRealDepth && spec.zIsotropy) ? (universe.random() * 2 - 1) * s.vyRange : 0;
|
|
213
|
+
} else { // 'beam' — collimated; vy=vz=0 stays (deliberate, theorist §4 option (b))
|
|
214
|
+
vx = Math.cos(spec.orientationRad) * spec.speed;
|
|
215
|
+
vy = Math.sin(spec.orientationRad) * spec.speed;
|
|
216
|
+
vz = 0;
|
|
217
|
+
}
|
|
218
|
+
var handle = universe.makeLeafFrame(entrance.x, entrance.y, spec.level, spec.wavelength, vx, vy, false, spec.charge, null);
|
|
219
|
+
universe.injectFrame(handle);
|
|
220
|
+
var qid = universe.getFrameQuantumId(handle);
|
|
221
|
+
if (spec.mass !== undefined && spec.mass !== null) {
|
|
222
|
+
universe.setFrameProperty(qid, 'mass', spec.mass);
|
|
223
|
+
}
|
|
224
|
+
if (entranceZ !== null) {
|
|
225
|
+
universe.setFrameProperty(qid, 'z', entranceZ);
|
|
226
|
+
if (vz !== 0) { universe.setFrameProperty(qid, 'vz', vz); }
|
|
227
|
+
}
|
|
228
|
+
placed.push({ handle: handle, qid: qid });
|
|
229
|
+
}
|
|
230
|
+
return placed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Build + register the ONE recurring hand-placed-quantum source tick-fn
|
|
235
|
+
* (kills the duplicated tunnel-lab / lightsource-lab injectors). LIVE knobs:
|
|
236
|
+
* every field of the returned `sourceState` is read FRESH on every burst —
|
|
237
|
+
* a UI slider mutating `sourceState.wavelength`/`density`/`rate`/`active`
|
|
238
|
+
* etc. takes effect on the NEXT burst, no re-registration.
|
|
239
|
+
* @param {Object} universe
|
|
240
|
+
* @param {Object} innerRect
|
|
241
|
+
* @param {boolean} capNegX
|
|
242
|
+
* @param {boolean} hasRealDepth
|
|
243
|
+
* @param {Object} [specPartial] - overrides onto SOURCE_SPEC_FIELDS' defaults.
|
|
244
|
+
* @returns {{ sourceState: SourceSpec, dispose: Function }}
|
|
245
|
+
*/
|
|
246
|
+
function buildSustainedSource(universe, innerRect, capNegX, hasRealDepth, specPartial) {
|
|
247
|
+
var sourceState = normalizeSourceSpec(specPartial);
|
|
248
|
+
|
|
249
|
+
var tickCounter = 0;
|
|
250
|
+
var injectorTickFn = function () {
|
|
251
|
+
if (!sourceState.active) { return; }
|
|
252
|
+
tickCounter++;
|
|
253
|
+
if (tickCounter % Math.max(1, sourceState.rate) !== 0) { return; }
|
|
254
|
+
placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, sourceState, sourceState.density);
|
|
255
|
+
};
|
|
256
|
+
// Tag the tick-fn with a back-reference (mirrors QuantumEmitter.dispose's
|
|
257
|
+
// OWN `fns[j]._emState` idiom EXACTLY, and both prior labs' own
|
|
258
|
+
// `_sourceInjector` tag) so dispose() below can find + splice out THIS
|
|
259
|
+
// EXACT closure, never a different injector that happens to also be
|
|
260
|
+
// registered.
|
|
261
|
+
injectorTickFn._sourceInjector = sourceState;
|
|
262
|
+
universe.experimentTickFunctions = universe.experimentTickFunctions || [];
|
|
263
|
+
universe.experimentTickFunctions.push(injectorTickFn);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
sourceState: sourceState,
|
|
267
|
+
// CANONICAL TEARDOWN (see reference_frqtlapp_setup_canonical_teardown.md):
|
|
268
|
+
// tombstone (defense in depth) then physically splice the tick-fn out
|
|
269
|
+
// of experimentTickFunctions by identity. Idempotent — a second call
|
|
270
|
+
// is a safe no-op.
|
|
271
|
+
dispose: function () {
|
|
272
|
+
sourceState.active = false;
|
|
273
|
+
var fns = universe.experimentTickFunctions;
|
|
274
|
+
if (!Array.isArray(fns)) { return; }
|
|
275
|
+
for (var j = fns.length - 1; j >= 0; j--) {
|
|
276
|
+
if (fns[j] === injectorTickFn) { fns.splice(j, 1); }
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* capacityAtDimension(dim) — the ONE authority for "capacity of a leaf at
|
|
284
|
+
* fundamental dimension `dim`" (Engine Invariant #6: `width=2^dim,
|
|
285
|
+
* capacity=width²`). Both tunnel-lab and lightsource-lab derive their
|
|
286
|
+
* `MAX_USEFUL_WAVELENGTH` slider ceiling from this (CEO drive #2 ceiling
|
|
287
|
+
* fix, ws-dse-3d AC4 — "same MAX_USEFUL_WAVELENGTH clamp authority") —
|
|
288
|
+
* never a hand-typed "16" in either factory.
|
|
289
|
+
* @param {number} dim
|
|
290
|
+
* @returns {number}
|
|
291
|
+
*/
|
|
292
|
+
function capacityAtDimension(dim) {
|
|
293
|
+
var width = Math.pow(2, dim);
|
|
294
|
+
return width * width;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
export { SOURCE_SPEC_FIELDS, SOURCE_SPEC_FIELD_NAMES, normalizeSourceSpec, placeSourceQuanta, buildSustainedSource, capacityAtDimension };
|
|
299
|
+
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tagger — the MARK part: put a user string on a frame, consistently.
|
|
5
|
+
*
|
|
6
|
+
* ── A Tagger IS … (the datasheet line; docs/internal/engine/THE_MINIMAL_SIM.md §3) ──
|
|
7
|
+
*
|
|
8
|
+
* A Tagger is the op-program op `{ set: '0.data.<key>', to: StrLit(<value>) }` — ONE
|
|
9
|
+
* write of a user string into a frame's engine-opaque `string_map` — offered on the
|
|
10
|
+
* two lanes a frame can be reached from, plus the matching read:
|
|
11
|
+
*
|
|
12
|
+
* IN-TICK (`tagOps`) — the op appended to a collision op-program; the engine runs
|
|
13
|
+
* it on every colliding frame (canonical bench LO7 MARK).
|
|
14
|
+
* HOST (`stamp`) — the SAME write between ticks, as a get→merge→set through
|
|
15
|
+
* `universe.getFrameProperty(qid,'data')` /
|
|
16
|
+
* `setFrameProperty(qid,'data',bag)`, for a frame that
|
|
17
|
+
* nothing collided with (no LO, no wall — see §Reuse).
|
|
18
|
+
* READ (`readTags`)— the tags back off a collected `FrameData`, without the
|
|
19
|
+
* caller ever naming `fd.data.string_map`.
|
|
20
|
+
*
|
|
21
|
+
* That is the whole part: three lanes, ONE key/value vocabulary, ZERO physics.
|
|
22
|
+
*
|
|
23
|
+
* ── Layer 1: API contract ───────────────────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* Tagger.tagOps(tags) → Array<op> (append to an op-program's ops)
|
|
26
|
+
* Tagger.stamp(universe, qid, tags) → number (keys written; host lane)
|
|
27
|
+
* Tagger.readTags(frameData) → {k:v} ({} when untagged)
|
|
28
|
+
* Tagger.readTag(frameData[, key]) → string|null (key defaults to MARK_KEY)
|
|
29
|
+
* Tagger.normalizeTags(tags) → {k:v} (the shared coercion; exported
|
|
30
|
+
* so a consumer never re-derives it)
|
|
31
|
+
* Tagger.MARK_KEY = 'detector_name' (the canonical provenance key)
|
|
32
|
+
*
|
|
33
|
+
* `tags` is either a plain object `{ key: value, … }` or a bare string (shorthand for
|
|
34
|
+
* `{ [MARK_KEY]: value }` — the "just mark it with this name" case that MARK/DETECT and
|
|
35
|
+
* the wall `mark` hook have always meant). Values are coerced with `String()`; `''` IS a
|
|
36
|
+
* valid tag value (the wall `mark` hook with no `markTag` stamps the empty string and has
|
|
37
|
+
* always done so — dropping it here would move a compiled program). `null`/`undefined`
|
|
38
|
+
* values are skipped. A non-string / empty key throws (FAIL-LOUD, never a silent no-op).
|
|
39
|
+
*
|
|
40
|
+
* Key order is the object's own insertion order, so `tagOps` is deterministic and a
|
|
41
|
+
* caller's program bytes are stable.
|
|
42
|
+
*
|
|
43
|
+
* ── The knobs ───────────────────────────────────────────────────────────────
|
|
44
|
+
*
|
|
45
|
+
* key — any string. It is carried VERBATIM by the engine, which interprets
|
|
46
|
+
* nothing (`lib/shim/frame-data` §string_map). A dotted key survives whole
|
|
47
|
+
* (`render.color` is a tag like any other — the wall `recolor` hook is a
|
|
48
|
+
* Tagger whose key happens to be one the renderer reads).
|
|
49
|
+
* value — any string. This is USER data; there is no enum, no registry, no engine
|
|
50
|
+
* meaning. Two different LOs stamping the same key is a last-write-wins
|
|
51
|
+
* overwrite inside one tick, in op order.
|
|
52
|
+
*
|
|
53
|
+
* ── Layer 2: physics behavior — measured constants ──────────────────────────
|
|
54
|
+
*
|
|
55
|
+
* ZERO physics (SU1). A tag write costs **0 RNG draws** (`StrLit` is a compile-time
|
|
56
|
+
* constant; `actionCompiler`'s own RNG-cost ledger already lists MARK at 0), touches no
|
|
57
|
+
* position, velocity, mass, capacity, wavelength or `is_alive`, and the engine
|
|
58
|
+
* interprets no `string_map` key. Measured baseline: the canonical bench holds
|
|
59
|
+
* **28/28** across this part landing, with **LO7 MARK + COUNT** the golden that covers
|
|
60
|
+
* it directly (`count=105, mark="probeA"` — the stamped string rides the collected
|
|
61
|
+
* FrameData and is read between ticks). A Tagger cannot move a golden; if one moves,
|
|
62
|
+
* physics leaked into the tag and that is a defect in this file.
|
|
63
|
+
*
|
|
64
|
+
* ── Reuse: a Tagger is NOT a wall part ──────────────────────────────────────
|
|
65
|
+
*
|
|
66
|
+
* The in-tick lane needs a collision, so it rides whatever object already hooks
|
|
67
|
+
* collisions (today: a `Wall`/`LargeObject` op-program — `Wall{hooks:['mark']}` IS a
|
|
68
|
+
* Tagger). The HOST lane needs nothing at all: a bare `experimentTickFunctions` entry
|
|
69
|
+
* that notices something about the world and stamps the frames it noticed is a complete
|
|
70
|
+
* detector with no wall, no rect and no collision — e.g. a thermostat-style probe that
|
|
71
|
+
* marks every frame it counts while the world is above its setpoint. See
|
|
72
|
+
* `lib/frqtl/objects/test/tagger.test.mjs` §"non-wall reuse".
|
|
73
|
+
*
|
|
74
|
+
* ── What this file does NOT own ─────────────────────────────────────────────
|
|
75
|
+
*
|
|
76
|
+
* The BUBBLE SCHEMA (`SECTION_KEYS`, `normalizeDataBubble`) is owned by
|
|
77
|
+
* `lib/shim/frame-data`; this file FERRIES it and never re-lists a section. The AST node
|
|
78
|
+
* kinds are owned by `lib/shim/expr/ast-schema.js`; this file consumes `StrLit` +
|
|
79
|
+
* `HANDLE_FRAME` and never re-derives an op shape. Which render keys are legal is owned
|
|
80
|
+
* by `RENDER_INTENT_KEYS`; a Tagger carries any key, exactly as the engine does.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
import * as AstSchema from '../../../shim/expr/ast-schema.js';
|
|
84
|
+
import * as FrameDataSchema from '../../../shim/frame-data/index.js';
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* MARK_KEY — the canonical provenance key. This is the key MARK/DETECT and the wall
|
|
90
|
+
* `mark` hook have always written, and the key `DetectorWall` reads for slit
|
|
91
|
+
* provenance. Named ONCE here so no consumer spells the string again.
|
|
92
|
+
*/
|
|
93
|
+
var MARK_KEY = 'detector_name';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* normalizeTags(tags) — the ONE coercion both write lanes share, so the in-tick op
|
|
97
|
+
* and the host stamp can never disagree about what "tagging with X" means.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object|string} tags — `{key: value, …}` or a bare string (→ {MARK_KEY: str}).
|
|
100
|
+
* @returns {Object} a fresh `{key: string}` bag (insertion order preserved).
|
|
101
|
+
* @throws {TypeError} on a non-object/non-string input, or a non-string/empty key.
|
|
102
|
+
*/
|
|
103
|
+
function normalizeTags(tags) {
|
|
104
|
+
if (typeof tags === 'string') {
|
|
105
|
+
var one = {};
|
|
106
|
+
one[MARK_KEY] = tags;
|
|
107
|
+
return one;
|
|
108
|
+
}
|
|
109
|
+
if (tags === null || typeof tags !== 'object' || Array.isArray(tags)) {
|
|
110
|
+
throw new TypeError('[Tagger] tags must be a plain object {key: value} or a bare ' +
|
|
111
|
+
'string (shorthand for {' + MARK_KEY + ': value}); got ' +
|
|
112
|
+
(Array.isArray(tags) ? 'an array' : typeof tags) + '.');
|
|
113
|
+
}
|
|
114
|
+
var out = {};
|
|
115
|
+
var keys = Object.keys(tags);
|
|
116
|
+
for (var i = 0; i < keys.length; i++) {
|
|
117
|
+
var k = keys[i];
|
|
118
|
+
if (typeof k !== 'string' || k.length === 0) {
|
|
119
|
+
throw new TypeError('[Tagger] tag key must be a non-empty string; got ' +
|
|
120
|
+
JSON.stringify(k) + '.');
|
|
121
|
+
}
|
|
122
|
+
var v = tags[k];
|
|
123
|
+
if (v === null || v === undefined) { continue; } // skipped, never stamped as "null"
|
|
124
|
+
out[k] = String(v);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* tagOps(tags) — the IN-TICK lane. Build the op-program ops that stamp `tags` onto the
|
|
131
|
+
* COLLIDING frame (`HANDLE_FRAME`), for a caller to concat into its own `ops` array.
|
|
132
|
+
*
|
|
133
|
+
* This is the single authority for the tag-op SHAPE: `actionCompiler`'s MARK/DETECT
|
|
134
|
+
* action, its v2 `mark` hook and its v2 `recolor` hook are all this function (the op
|
|
135
|
+
* bytes are unchanged — the compiler ferries them, it does not re-spell them).
|
|
136
|
+
*
|
|
137
|
+
* @param {Object|string} tags
|
|
138
|
+
* @returns {Array<{set:string, to:Object}>} — one op per key, in key order.
|
|
139
|
+
*/
|
|
140
|
+
function tagOps(tags) {
|
|
141
|
+
var ast = AstSchema;
|
|
142
|
+
var bag = normalizeTags(tags);
|
|
143
|
+
var ops = [];
|
|
144
|
+
var keys = Object.keys(bag);
|
|
145
|
+
for (var i = 0; i < keys.length; i++) {
|
|
146
|
+
// `0.data.<key>` — the op-apply layer intercepts StrLit and routes it to the
|
|
147
|
+
// frame's string_map write path (the dotted part of a key like 'render.color'
|
|
148
|
+
// survives whole: strKey = prop.slice(5)).
|
|
149
|
+
ops.push({ set: ast.HANDLE_FRAME + '.data.' + keys[i], to: ast.StrLit(bag[keys[i]]) });
|
|
150
|
+
}
|
|
151
|
+
return ops;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* stamp(universe, quantumId, tags) — the HOST lane. Tag a frame BETWEEN ticks, with
|
|
156
|
+
* no collision, no LargeObject and no op-program: read the frame's current data bag,
|
|
157
|
+
* merge the tags into its `string_map`, write the whole bag back.
|
|
158
|
+
*
|
|
159
|
+
* get→merge→set is deliberate (the architect's render-intent stamp ruling): the write
|
|
160
|
+
* must PRESERVE the frame's `detection` section and any pre-existing string_map keys,
|
|
161
|
+
* rather than relying on the engine's `set('data')` happening to merge.
|
|
162
|
+
*
|
|
163
|
+
* @param {Object} universe — an EGPTUniverse (get/setFrameProperty accessors).
|
|
164
|
+
* @param {number} quantumId — the frame's QUANTUM ID (`FrameData.id` — NOT the
|
|
165
|
+
* `FrameId` handle `makeLeafFrame` returns; bridge with `getFrameQuantumId`).
|
|
166
|
+
* @param {Object|string} tags
|
|
167
|
+
* @returns {number} the number of keys written.
|
|
168
|
+
* @throws {TypeError} on a universe without the accessors, or a bad tag bag.
|
|
169
|
+
*/
|
|
170
|
+
function stamp(universe, quantumId, tags) {
|
|
171
|
+
if (!universe || typeof universe.setFrameProperty !== 'function') {
|
|
172
|
+
throw new TypeError('[Tagger.stamp] needs a universe with setFrameProperty ' +
|
|
173
|
+
'(the host write lane). Got ' + (universe ? typeof universe : universe) + '.');
|
|
174
|
+
}
|
|
175
|
+
var bag = normalizeTags(tags);
|
|
176
|
+
var keys = Object.keys(bag);
|
|
177
|
+
if (keys.length === 0) { return 0; }
|
|
178
|
+
|
|
179
|
+
var current = null;
|
|
180
|
+
if (typeof universe.getFrameProperty === 'function') {
|
|
181
|
+
current = universe.getFrameProperty(quantumId, 'data');
|
|
182
|
+
}
|
|
183
|
+
if (!current || typeof current !== 'object') { current = {}; }
|
|
184
|
+
var sm = (current.string_map && typeof current.string_map === 'object')
|
|
185
|
+
? current.string_map : {};
|
|
186
|
+
for (var i = 0; i < keys.length; i++) { sm[keys[i]] = bag[keys[i]]; }
|
|
187
|
+
current.string_map = sm;
|
|
188
|
+
universe.setFrameProperty(quantumId, 'data', current);
|
|
189
|
+
return keys.length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* readTags(frameData) — the READ lane. Pull every tag off a collected `FrameData`
|
|
194
|
+
* (`tickData.objects[handle].frames[i]`, or `tickData.frameData[i]`) without the
|
|
195
|
+
* caller naming `fd.data.string_map`.
|
|
196
|
+
*
|
|
197
|
+
* The bubble shape comes from its owner (`lib/shim/frame-data` `normalizeDataBubble`),
|
|
198
|
+
* so a section change lands here for free and this file never re-lists SECTION_KEYS.
|
|
199
|
+
*
|
|
200
|
+
* @param {Object} frameData — a FrameData (or anything carrying `.data`).
|
|
201
|
+
* @returns {Object} `{key: string}` — `{}` when the frame carries no tags.
|
|
202
|
+
*/
|
|
203
|
+
function readTags(frameData) {
|
|
204
|
+
if (!frameData) { return {}; }
|
|
205
|
+
var bubble = FrameDataSchema.normalizeDataBubble(frameData.data);
|
|
206
|
+
var sm = bubble.string_map;
|
|
207
|
+
if (!sm || typeof sm !== 'object') { return {}; }
|
|
208
|
+
var out = {};
|
|
209
|
+
var keys = Object.keys(sm);
|
|
210
|
+
for (var i = 0; i < keys.length; i++) {
|
|
211
|
+
if (sm[keys[i]] !== null && sm[keys[i]] !== undefined) { out[keys[i]] = sm[keys[i]]; }
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* readTag(frameData, key) — one tag off a collected FrameData.
|
|
218
|
+
* @param {Object} frameData
|
|
219
|
+
* @param {string} [key=MARK_KEY] — defaults to the canonical provenance key.
|
|
220
|
+
* @returns {string|null} the tag value, or null when the frame is untagged.
|
|
221
|
+
*/
|
|
222
|
+
function readTag(frameData, key) {
|
|
223
|
+
var k = (key === undefined || key === null) ? MARK_KEY : key;
|
|
224
|
+
var tags = readTags(frameData);
|
|
225
|
+
return (tags[k] !== undefined) ? tags[k] : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { MARK_KEY, normalizeTags, tagOps, stamp, readTags, readTag };
|