@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,116 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DataCollector — Base class for tick-by-tick metric sampling.
|
|
5
|
+
*
|
|
6
|
+
* Subclasses override collectMetrics() to return experiment-specific
|
|
7
|
+
* key/value pairs. The base class handles sample scheduling, time-series
|
|
8
|
+
* accumulation, and result formatting.
|
|
9
|
+
*
|
|
10
|
+
* Works with both the JS engine (property getter for activeQuanta) and
|
|
11
|
+
* the WASM engine (method call for activeQuanta).
|
|
12
|
+
*
|
|
13
|
+
* An ES module: `export { DataCollector }`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get active quanta count from a universe, handling both the JS engine
|
|
20
|
+
* (property getter) and the WASM engine (method call) patterns.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} universe
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
function getActiveQuanta(universe) {
|
|
26
|
+
if (typeof universe.activeQuanta === 'function') {
|
|
27
|
+
return universe.activeQuanta();
|
|
28
|
+
}
|
|
29
|
+
return universe.activeQuanta;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── DataCollector ────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Base collector for tick-by-tick metric sampling.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} [options]
|
|
38
|
+
* @param {number} [options.sampleInterval=100] - Record a sample every N ticks
|
|
39
|
+
* @param {Object} [options.params] - Experiment params to attach to results
|
|
40
|
+
*/
|
|
41
|
+
function DataCollector(options) {
|
|
42
|
+
options = options || {};
|
|
43
|
+
this.sampleInterval = options.sampleInterval !== undefined ? options.sampleInterval : 100;
|
|
44
|
+
this.params = options.params || {};
|
|
45
|
+
this.timeSeries = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Called every tick by the runner. Records a sample if
|
|
50
|
+
* tick % sampleInterval === 0.
|
|
51
|
+
*
|
|
52
|
+
* @param {number} tick - Current tick number (1-based)
|
|
53
|
+
* @param {Object} universe - Engine universe instance
|
|
54
|
+
* @param {Object} setupResult - Object returned by the setup factory
|
|
55
|
+
* @returns {Object|null} The sample object, or null if not a sample tick
|
|
56
|
+
*/
|
|
57
|
+
DataCollector.prototype.sample = function (tick, universe, setupResult) {
|
|
58
|
+
if (tick % this.sampleInterval !== 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var metrics = this.collectMetrics(universe, setupResult);
|
|
63
|
+
var sample = { tick: tick };
|
|
64
|
+
|
|
65
|
+
// Merge metrics into sample
|
|
66
|
+
for (var key in metrics) {
|
|
67
|
+
if (metrics.hasOwnProperty(key)) {
|
|
68
|
+
sample[key] = metrics[key];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.timeSeries.push(sample);
|
|
73
|
+
return sample;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Override in subclasses to return experiment-specific metrics.
|
|
78
|
+
* The base implementation records only activeQuanta.
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} universe - Engine universe instance
|
|
81
|
+
* @param {Object} setupResult - Object returned by the setup factory
|
|
82
|
+
* @returns {Object} Flat object of metric key/value pairs
|
|
83
|
+
*/
|
|
84
|
+
DataCollector.prototype.collectMetrics = function (universe, setupResult) {
|
|
85
|
+
return {
|
|
86
|
+
activeQuanta: getActiveQuanta(universe)
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Return all collected data.
|
|
92
|
+
*
|
|
93
|
+
* @returns {{ params: Object, timeSeries: Array }}
|
|
94
|
+
*/
|
|
95
|
+
DataCollector.prototype.getResults = function () {
|
|
96
|
+
return {
|
|
97
|
+
params: this.params,
|
|
98
|
+
timeSeries: this.timeSeries.slice()
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reset state for the next seed run.
|
|
104
|
+
*/
|
|
105
|
+
DataCollector.prototype.reset = function () {
|
|
106
|
+
this.timeSeries = [];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ── Static helper (re-exported for subclass convenience) ─────────────
|
|
110
|
+
|
|
111
|
+
DataCollector.getActiveQuanta = getActiveQuanta;
|
|
112
|
+
|
|
113
|
+
// ── Export ───────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export { DataCollector };
|
|
116
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WallBehaviors — the ONE authority for the Collision LEXICON v2: the wall
|
|
5
|
+
* vocabulary (`collisionBehavior` × `bounceDirection` × composable hooks × presets)
|
|
6
|
+
* and the structured resolver that turns a wall spec into a compiled op-program.
|
|
7
|
+
*
|
|
8
|
+
* ── Schema-mirror discipline (architect W1(ii)/(iv) guard) ───────────────────
|
|
9
|
+
* The hook table FERRIES the frozen `CollisionActions` constant set BY REFERENCE —
|
|
10
|
+
* it NEVER re-types a SCREAMING_SNAKE string. The vocabulary keys ARE the enum: the
|
|
11
|
+
* `Wall` descriptor's enum fields read these maps (never a hand-listed enum),
|
|
12
|
+
* `Wall.js` reads the resolver, and the conformance/compiler tests drive their
|
|
13
|
+
* expectations off these same tables — so a behavior that works through one wall
|
|
14
|
+
* composite but not another is impossible: they all compile through the SAME
|
|
15
|
+
* `actionCompiler` from the SAME owned constants.
|
|
16
|
+
*
|
|
17
|
+
* ── The behavior axis (all compiled to op-programs by actionCompiler.js) ─────
|
|
18
|
+
* nothing — pass through (no ops at all).
|
|
19
|
+
* block — STOP at the surface: eject to the closest position outside, velocity
|
|
20
|
+
* KEPT (it parks at the face and keeps pushing).
|
|
21
|
+
* bounce — TURN AROUND: eject + a direction response (`bounceDirection`:
|
|
22
|
+
* reflect = the incidence flip, random = a scatter redraw).
|
|
23
|
+
* absorb — the RADIATING absorber: kill + tally the mass into the wall's REAL
|
|
24
|
+
* backing frame + re-radiate (conserving).
|
|
25
|
+
* destroy — the plain kill (the mass sink at an experiment boundary).
|
|
26
|
+
* Hooks (`mark`/`count`/`recolor`) compose with ANY behavior.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { CollisionTypes } from '../../parts/CollisionTypes.js';
|
|
30
|
+
import { CollisionActions } from '../../parts/CollisionActions.js';
|
|
31
|
+
import { compileWallV2 } from '../objects/actionCompiler.js';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
36
|
+
// Collision LEXICON v2 (CEO 3e2016aa) — the STUDIO/palette surface and the ONLY
|
|
37
|
+
// wall vocabulary. Three ferried vocab tables + a structured resolver that
|
|
38
|
+
// delegates to the ONE compiler authority (compileWallV2 in actionCompiler.js).
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
// behavior axis (ONE of): nothing | block | bounce | absorb | destroy. Keys ARE the enum.
|
|
42
|
+
// Ordered by escalating response to a hit: pass through → stop → turn → consume → remove.
|
|
43
|
+
// nothing — pass-through (no ops at all).
|
|
44
|
+
// block — STOP the particle AT the surface: reposition it to the closest position
|
|
45
|
+
// OUTSIDE the wall, velocity UNTOUCHED. Literally `bounce` minus the reflect
|
|
46
|
+
// ops. MEASURED head-to-head against `bounce` on the canonical WASM engine
|
|
47
|
+
// (wall x∈[200,220], leaf launched x=150 vx=+1): both stop penetration at
|
|
48
|
+
// x=199, but `bounce` flips to vx=−1 and has RECEDED to x=158.5 by t=90,
|
|
49
|
+
// while `block` keeps vx=+1 and PARKS at x=198.5 for every remaining tick.
|
|
50
|
+
// Same contact tick (45), opposite outcome — so `bounce` is NOT a stand-in
|
|
51
|
+
// for `block` (the eject-only op-set is the RESISTANCE intuition: a particle
|
|
52
|
+
// cannot move into occupied space, and nothing gives it a reason to turn).
|
|
53
|
+
// bounce — a motion response (see bounceDirection).
|
|
54
|
+
// absorb — kill + tally into the wall's REAL backing frame + radiate (conserving).
|
|
55
|
+
// destroy — plain kill.
|
|
56
|
+
var WALL_BEHAVIORS = { nothing: 'nothing', block: 'block', bounce: 'bounce', absorb: 'absorb', destroy: 'destroy' };
|
|
57
|
+
// bounceDirection (when behavior==bounce): reflect | random.
|
|
58
|
+
var BOUNCE_DIRECTIONS = { reflect: 'reflect', random: 'random' };
|
|
59
|
+
// hooks (composable with ANY behavior) — FERRIED from the frozen CollisionActions set.
|
|
60
|
+
// `mark`/`count` FERRY the frozen engine CollisionActions. `recolor` is a RENDER-INTENT
|
|
61
|
+
// hook (not an engine action — the engine has no color, I1): it stamps the wall's OWN
|
|
62
|
+
// drawn color into the collided frame's opaque string_map['render.color'], which the
|
|
63
|
+
// renderer providers already read. It is MARK's sibling (a string_map write the engine
|
|
64
|
+
// passes through), so it lives in the SAME composable-hooks slot; its value is a plain
|
|
65
|
+
// marker (there is no CollisionActions.RECOLOR to ferry).
|
|
66
|
+
var WALL_HOOKS = { mark: CollisionActions.MARK, count: CollisionActions.COUNT, recolor: 'recolor' };
|
|
67
|
+
|
|
68
|
+
function _valuesMap(obj) { var m = {}; Object.keys(obj).forEach(function (k) { m[k] = k; }); return m; }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the v2 nested wall spec to a compiled op-program + flags via the ONE
|
|
72
|
+
* compiler authority (compileWallV2). FAIL-LOUD on an unknown behavior/direction.
|
|
73
|
+
* @param {Object} props — { collisionBehavior, bounceDirection, angleConstraint,
|
|
74
|
+
* decoherence, hooks, markTag, color, capacity, emissivity, reposition, orientationDeg }
|
|
75
|
+
* @returns {{ program, collectCollided, radiation, behavior, hooks }}
|
|
76
|
+
*/
|
|
77
|
+
function resolveWallSpec(props) {
|
|
78
|
+
props = props || {};
|
|
79
|
+
var behavior = props.collisionBehavior || 'nothing';
|
|
80
|
+
if (!WALL_BEHAVIORS[behavior]) {
|
|
81
|
+
throw new Error('[WallBehaviors] unknown collisionBehavior "' + behavior + '" — must be one of [' +
|
|
82
|
+
Object.keys(WALL_BEHAVIORS).join(', ') + '].');
|
|
83
|
+
}
|
|
84
|
+
if (behavior === 'bounce' && props.bounceDirection && !BOUNCE_DIRECTIONS[props.bounceDirection]) {
|
|
85
|
+
throw new Error('[WallBehaviors] unknown bounceDirection "' + props.bounceDirection + '" — must be one of [' +
|
|
86
|
+
Object.keys(BOUNCE_DIRECTIONS).join(', ') + '].');
|
|
87
|
+
}
|
|
88
|
+
var hooks = Array.isArray(props.hooks) ? props.hooks : [];
|
|
89
|
+
hooks.forEach(function (hk) {
|
|
90
|
+
if (!WALL_HOOKS[hk]) {
|
|
91
|
+
throw new Error('[WallBehaviors] unknown hook "' + hk + '" — must be one of [' +
|
|
92
|
+
Object.keys(WALL_HOOKS).join(', ') + '].');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
var compiled = compileWallV2({
|
|
96
|
+
behavior: behavior,
|
|
97
|
+
bounceDirection: props.bounceDirection || 'reflect',
|
|
98
|
+
angleConstraint: props.angleConstraint,
|
|
99
|
+
orientationDeg: props.orientationDeg,
|
|
100
|
+
decoherence: props.decoherence,
|
|
101
|
+
hooks: hooks,
|
|
102
|
+
markTag: props.markTag,
|
|
103
|
+
// The wall's OWN drawn color — the ONE authority the `recolor` hook derives its
|
|
104
|
+
// render.color tag from at compile time (ferried, never a parallel color field).
|
|
105
|
+
color: props.color,
|
|
106
|
+
capacity: props.capacity,
|
|
107
|
+
// ABSORB radiating efficiency (fullness fraction of the emitted leaf). Omitted ⇒
|
|
108
|
+
// absorbRadiation's own default — no second copy of the default lives here.
|
|
109
|
+
emissivity: props.emissivity,
|
|
110
|
+
reposition: props.reposition || 'closest',
|
|
111
|
+
// Z1 — a real z-slab wall reflects/ejects on all six faces (3-axis incidence
|
|
112
|
+
// + closest3d eject). Derived from the caller (Wall.js reads it off the rect);
|
|
113
|
+
// omitted/false ⇒ the 2D path (byte-identical, SU1).
|
|
114
|
+
is3D: props.is3D === true,
|
|
115
|
+
// MIRROR ruling — the TRUE slab dims (Wall reads them off the rect) so a tilted 2D
|
|
116
|
+
// reflect wall can bake the OBB-NORMAL eject (perpendicular to the drawn face) instead
|
|
117
|
+
// of the beam-scattering AABB 'closest'. Omitted ⇒ the AABB eject (byte-identical, SU1).
|
|
118
|
+
slabW: props.slabW,
|
|
119
|
+
slabH: props.slabH,
|
|
120
|
+
// R2AX (CEO two-axis ruling 2026-07-26) — the plate's up/down tilt + z-depth,
|
|
121
|
+
// ferried so a pitched plate compiles the 3-space reflection about SlabFace's
|
|
122
|
+
// plate3.n. Omitted ⇒ byte-identical un-pitched programs (SU1).
|
|
123
|
+
pitchDeg: props.pitchDeg,
|
|
124
|
+
slabD: props.slabD
|
|
125
|
+
});
|
|
126
|
+
return { program: compiled.program, collectCollided: compiled.collectCollided,
|
|
127
|
+
radiation: compiled.radiation, behavior: behavior, hooks: hooks.slice() };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Presets (B6) — pure CONSTRUCTION sugar expanding to the v2 descriptor fields.
|
|
131
|
+
// The recipe/codegen round-trips the EXPANDED fields (never the preset name).
|
|
132
|
+
//
|
|
133
|
+
// THE NAMING RULE (CEO ruling, 2026-07-31): a preset's NAME MUST ASSERT ITS BEHAVIOR.
|
|
134
|
+
// `collisionBehavior:'nothing'` (pass-through) belongs ONLY on a preset whose name SAYS
|
|
135
|
+
// it passes through — `transparent-probe` is the one that earns it. A palette entry
|
|
136
|
+
// labelled "Wall" that particles walk through is the same defect class as mistaking
|
|
137
|
+
// `Tunnel{mode:'mirror'}` for a mirror: a name asserting behavior the part does not have.
|
|
138
|
+
// Before `block` existed there was no blocking behavior in the lexicon to point `wall` at,
|
|
139
|
+
// so `wall → nothing` was as honest as the vocabulary allowed — and simultaneously
|
|
140
|
+
// REDUNDANT with `transparent-probe`. `block` removed that reason, so `wall` now blocks.
|
|
141
|
+
// Apply this rule to every preset added here.
|
|
142
|
+
var WALL_PRESETS = {
|
|
143
|
+
'mirror': { label: 'Mirror', collisionBehavior: 'bounce', bounceDirection: 'reflect' },
|
|
144
|
+
'brick': { label: 'Brick Wall', collisionBehavior: 'bounce', bounceDirection: 'random', decoherence: false },
|
|
145
|
+
'detector-screen': { label: 'Detector Screen', collisionBehavior: 'destroy', hooks: ['count'], countFilter: 'any' },
|
|
146
|
+
'transparent-probe': { label: 'Transparent Probe', collisionBehavior: 'nothing', hooks: ['mark', 'count'] },
|
|
147
|
+
// A "Wall" is SOLID: it stops the particle at the surface, velocity kept (see `block`
|
|
148
|
+
// above). For a wall-shaped instrument that does NOT stop anything, use
|
|
149
|
+
// `transparent-probe` — that is what its name is for.
|
|
150
|
+
'wall': { label: 'Wall', collisionBehavior: 'block' }
|
|
151
|
+
};
|
|
152
|
+
/** Expand a preset key to its v2 descriptor-field bag (FRESH copy). FAIL-LOUD on unknown. */
|
|
153
|
+
function expandWallPreset(key) {
|
|
154
|
+
var p = WALL_PRESETS[key];
|
|
155
|
+
if (!p) {
|
|
156
|
+
throw new Error('[WallBehaviors] unknown wall preset "' + key + '" — must be one of [' +
|
|
157
|
+
Object.keys(WALL_PRESETS).join(', ') + '].');
|
|
158
|
+
}
|
|
159
|
+
var out = {}; Object.keys(p).forEach(function (k) { if (k !== 'label') { out[k] = Array.isArray(p[k]) ? p[k].slice() : p[k]; } });
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function wallBehaviorV2ValuesMap() { return _valuesMap(WALL_BEHAVIORS); }
|
|
164
|
+
function bounceDirectionValuesMap() { return _valuesMap(BOUNCE_DIRECTIONS); }
|
|
165
|
+
function wallHookValuesMap() { return _valuesMap(WALL_HOOKS); }
|
|
166
|
+
|
|
167
|
+
// Collision LEXICON v2 — the ONE wall vocabulary.
|
|
168
|
+
export {
|
|
169
|
+
WALL_BEHAVIORS,
|
|
170
|
+
BOUNCE_DIRECTIONS,
|
|
171
|
+
WALL_HOOKS,
|
|
172
|
+
wallBehaviorV2ValuesMap,
|
|
173
|
+
bounceDirectionValuesMap,
|
|
174
|
+
wallHookValuesMap,
|
|
175
|
+
resolveWallSpec,
|
|
176
|
+
WALL_PRESETS,
|
|
177
|
+
expandWallPreset
|
|
178
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SlabFace.js — the SOLE face / OBB / convention authority for a rotated Wall slab.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this file exists (the DRY crux — architect Gate-2 ruling, circulation §1)
|
|
7
|
+
*
|
|
8
|
+
* A Wall mirror's reflecting FACE is derived from its `orientationDeg` in what USED to be
|
|
9
|
+
* three independent, hand-authored places, each re-writing the SAME "tall slab, y-DOWN
|
|
10
|
+
* screen handedness" convention:
|
|
11
|
+
* 1. the renderer draws the slab as `rect(-w/2,-h/2,w,h)` (h>w) rotated by +orientationDeg;
|
|
12
|
+
* 2. `actionCompiler._buildReflectOps` baked `alphaScreen = (90 + orientationDeg) % 360`
|
|
13
|
+
* (the drawn-tall-face reflection line);
|
|
14
|
+
* 3. the engine swept-OBB narrow-phase needs the slab's rotated collision region.
|
|
15
|
+
*
|
|
16
|
+
* A hand-mirrored sign in ANY of the three re-creates the off-face deflection bug the CEO
|
|
17
|
+
* saw when rotating a mirror (the "1A/1D" class). This module is the ONE place the
|
|
18
|
+
* convention is written: `orientationDeg (+ slab aspect, z-slab) → canonical face geometry`.
|
|
19
|
+
* `_buildReflectOps` reads `reflectBaseDeg` from here; `Wall.compileWallV2` reads `obb` from
|
|
20
|
+
* here for the engine `region` field. The renderer consumes the RAW `orientationDeg` (the
|
|
21
|
+
* primary reference this convention is DEFINED to match — no second convention there).
|
|
22
|
+
*
|
|
23
|
+
* ## The convention (single authority — do not re-derive elsewhere)
|
|
24
|
+
*
|
|
25
|
+
* - **Drawn face.** The Wall mirror is a TALL slab (h > w). At orientation 0 its long face
|
|
26
|
+
* is VERTICAL — the line at 90° in the engine's y-DOWN velocity coordinates — so a +x beam
|
|
27
|
+
* bounces straight back (−x), exactly what the cardinal incidence tree does. p5/three draw
|
|
28
|
+
* the slab rotated by +orientationDeg, so the drawn face LINE sits at `(90 + orientationDeg)`.
|
|
29
|
+
* ⇒ `reflectBaseDeg = (90 + orientationDeg) % 360` — reflect about the line the user SEES.
|
|
30
|
+
* - **OBB geometry (convention-FREE, the ONLY thing that crosses the ABI).** The engine
|
|
31
|
+
* deserializes `{ theta, hw, hh, hz }` and does a segment-vs-OBB test in ITS OWN coordinate
|
|
32
|
+
* frame (`region.rs`, `Obb::to_local` rotates world by −theta). The slab's local x-axis
|
|
33
|
+
* (thin, `hw = w/2`) and local y-axis (tall, `hh = h/2`) are world-rotated by +orientationDeg
|
|
34
|
+
* to match the drawn rect. ⇒ `theta = orientationDeg` (RADIANS). There is NO "90+" in the
|
|
35
|
+
* OBB — the reflection base and the OBB angle are DIFFERENT derived views of one input, and
|
|
36
|
+
* the raw `orientationDeg` NEVER crosses to the engine (storing it there = the convention-
|
|
37
|
+
* drift bug the circulation rejects).
|
|
38
|
+
*
|
|
39
|
+
* ## Cardinal gate (one threshold, shared with the reflect-law cardinal branch)
|
|
40
|
+
*
|
|
41
|
+
* `isCardinal` (orientationDeg ≡ 0 mod 90 within EPS) routes BOTH: (a) `_buildReflectOps` to
|
|
42
|
+
* the axis-aligned incidence tree, and (b) the engine to the byte-identical AABB path
|
|
43
|
+
* (`obb === null` ⇒ the sugar emits `region: null`). Aligning the two gates on one authority
|
|
44
|
+
* means "is this cardinal" is answered ONCE. At a cardinal, `obb` is `null` (byte-identical,
|
|
45
|
+
* SU1) — the region is the AABB and nothing rotated is ever sent.
|
|
46
|
+
*
|
|
47
|
+
* ## Layer 1 — API contract
|
|
48
|
+
*
|
|
49
|
+
* `resolveSlabFace({ cx, cy, w, h, orientationDeg, z, d }) → {`
|
|
50
|
+
* `isCardinal: boolean,` // orientationDeg ≡ 0 (mod 90) within EPS
|
|
51
|
+
* `reflectBaseDeg: number,` // (90 + orientationDeg) % 360 — the value _buildReflectOps bakes
|
|
52
|
+
* `obb: {` // convention-RESOLVED geometry (null when isCardinal)
|
|
53
|
+
* `cx, cy, cz,` // center (LO backing-frame center at collide time; JS-side/tests)
|
|
54
|
+
* `hw, hh, hz,` // half-extents (w/2, h/2, d/2) — hz 0 for a 2D slab
|
|
55
|
+
* `theta,` // RADIANS, engine y-DOWN frame — the ONLY angle that crosses the ABI
|
|
56
|
+
* `faceDir: { x, y },` // unit drawn-face LINE dir, ENGINE (y-down) frame = the reflect
|
|
57
|
+
* // law's invariant line (JS-side predicates; not on the ABI)
|
|
58
|
+
* `normal: { x, y }` // faceDir ⟂ (+90) = the axis R(reflectBaseDeg) FLIPS — the
|
|
59
|
+
* // tilted-reflect guard's crossing normal (sign conventional;
|
|
60
|
+
* // the guard is n→−n invariant). Not on the ABI.
|
|
61
|
+
* `} | null`
|
|
62
|
+
* `}`
|
|
63
|
+
* @param {Object} spec
|
|
64
|
+
* @param {number} [spec.cx=0] emission/collision center X (px) — the LO re-centered center.
|
|
65
|
+
* @param {number} [spec.cy=0] center Y (px).
|
|
66
|
+
* @param {number} spec.w slab width (px, physics). Thin axis for a tall mirror.
|
|
67
|
+
* @param {number} spec.h slab height (px, physics). Long face axis for a tall mirror.
|
|
68
|
+
* @param {number} [spec.orientationDeg=0] mirror SURFACE angle (deg). Normalized to [0,360).
|
|
69
|
+
* @param {number} [spec.z=0] slab z-center (px) — 3D wall; 0 for 2D.
|
|
70
|
+
* @param {number} [spec.d=0] slab z-depth (px) — 3D wall; 0/absent ⇒ hz 0 (2D passthrough).
|
|
71
|
+
* @param {number} [spec.pitchDeg=0] up/down tilt about the plate's own long axis (deg,
|
|
72
|
+
* [-90,90]; 3D wall REQUIRED). ≠0 ⇒ the PITCHED result shape: `{ pitched:true, obb:null,
|
|
73
|
+
* plate3:{ n,u,v, hw,hh,hz, aabb } }` — the true 3-space plate basis; NO engine region
|
|
74
|
+
* (z-theta-only contract; the in-op guard is the narrow phase). 0/absent ⇒ byte-identical
|
|
75
|
+
* un-pitched behavior (this whole block is opt-in — CEO two-axis ruling 2026-07-26).
|
|
76
|
+
*
|
|
77
|
+
* ## Layer 2 — physics behavior
|
|
78
|
+
* ZERO physics of its own (SU1). This is a pure geometry derivation consumed by the reflect
|
|
79
|
+
* op-program (velocity RESPONSE) and the engine narrow-phase (collision MEMBERSHIP). Rotating
|
|
80
|
+
* the mirror sweeps the drawn face; `reflectBaseDeg`/`theta` follow it so the bounce happens
|
|
81
|
+
* AT the face the user sees and the collision REGION is that same rotated slab. A moved bench
|
|
82
|
+
* golden would mean physics leaked into this convenience layer — the SU1 bug.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
var EPS = 1e-9;
|
|
88
|
+
var DEG2RAD = Math.PI / 180;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a slab's face convention + OBB geometry from ONE input (orientationDeg).
|
|
92
|
+
* See the file header for the full contract. Convention-free geometry only crosses
|
|
93
|
+
* the ABI (theta + half-extents); the raw orientationDeg NEVER does.
|
|
94
|
+
* @param {Object} spec
|
|
95
|
+
* @returns {{ isCardinal: boolean, reflectBaseDeg: number, obb: Object|null }}
|
|
96
|
+
*/
|
|
97
|
+
function resolveSlabFace(spec) {
|
|
98
|
+
spec = spec || {};
|
|
99
|
+
var w = +spec.w;
|
|
100
|
+
var h = +spec.h;
|
|
101
|
+
var o = (typeof spec.orientationDeg === 'number' && isFinite(spec.orientationDeg))
|
|
102
|
+
? spec.orientationDeg : 0;
|
|
103
|
+
// ── PITCH (two-axis mirror, CEO ruling 2026-07-26) ─────────────────────────
|
|
104
|
+
// `pitchDeg` rotates the plate about its OWN in-plane LONG axis (the drawn face
|
|
105
|
+
// line), tipping the face normal out of the xy-plane: positive pitch tips the
|
|
106
|
+
// normal toward +z. The TRUE 3-space plate basis:
|
|
107
|
+
// u = (cos rb, sin rb, 0) — long axis (pitch rotation axis)
|
|
108
|
+
// n = (n2.x·cos p, n2.y·cos p, sin p) — face normal (the reflection axis)
|
|
109
|
+
// v = u × n — depth axis (pitched)
|
|
110
|
+
// pitch 0 (or absent) is BYTE-IDENTICAL to the un-pitched contract (the z-axis
|
|
111
|
+
// orientation law with vz passing through) — the pitched branch below is opt-in.
|
|
112
|
+
// A pitched plate has NO engine ObbRegion (the region contract is z-theta-only):
|
|
113
|
+
// callers emit region null and use the plate's 3D AABB as the broad phase; the
|
|
114
|
+
// in-op guard (actionCompiler) is the narrow phase. Engine two-angle OBB is a
|
|
115
|
+
// flagged Gate-1 follow-up.
|
|
116
|
+
var p = (typeof spec.pitchDeg === 'number' && isFinite(spec.pitchDeg)) ? spec.pitchDeg : 0;
|
|
117
|
+
if (Math.abs(p) > EPS) {
|
|
118
|
+
if (p < -90 || p > 90) {
|
|
119
|
+
throw new Error('[SlabFace] pitchDeg must be within [-90, 90]; got ' + p);
|
|
120
|
+
}
|
|
121
|
+
var dP = (typeof spec.d === 'number' && isFinite(spec.d) && spec.d > 0) ? spec.d : 0;
|
|
122
|
+
if (!(dP > 0)) {
|
|
123
|
+
throw new Error('[SlabFace] a PITCHED plate requires a real z-depth (3D wall) — ' +
|
|
124
|
+
'pitchDeg ' + p + ' with d=' + spec.d + '. Give the wall a z/depth first.');
|
|
125
|
+
}
|
|
126
|
+
var onormP = ((o % 360) + 360) % 360;
|
|
127
|
+
var rbP = (90 + onormP) % 360;
|
|
128
|
+
var gP = rbP * DEG2RAD;
|
|
129
|
+
var prad = p * DEG2RAD;
|
|
130
|
+
var u3 = { x: Math.cos(gP), y: Math.sin(gP), z: 0 };
|
|
131
|
+
var n2p = { x: -Math.sin(gP), y: Math.cos(gP) };
|
|
132
|
+
var n3 = { x: n2p.x * Math.cos(prad), y: n2p.y * Math.cos(prad), z: Math.sin(prad) };
|
|
133
|
+
var v3 = { // u × n
|
|
134
|
+
x: u3.y * n3.z - u3.z * n3.y,
|
|
135
|
+
y: u3.z * n3.x - u3.x * n3.z,
|
|
136
|
+
z: u3.x * n3.y - u3.y * n3.x
|
|
137
|
+
};
|
|
138
|
+
var hwP = w / 2, hhP = h / 2, hzP = dP / 2;
|
|
139
|
+
var cxP = (typeof spec.cx === 'number' && isFinite(spec.cx)) ? spec.cx : 0;
|
|
140
|
+
var cyP = (typeof spec.cy === 'number' && isFinite(spec.cy)) ? spec.cy : 0;
|
|
141
|
+
var czP = (typeof spec.z === 'number' && isFinite(spec.z)) ? spec.z : 0;
|
|
142
|
+
return {
|
|
143
|
+
isCardinal: false,
|
|
144
|
+
pitched: true,
|
|
145
|
+
reflectBaseDeg: rbP,
|
|
146
|
+
obb: null, // NO engine region for a pitched plate (z-theta-only contract)
|
|
147
|
+
plate3: {
|
|
148
|
+
cx: cxP, cy: cyP, cz: czP,
|
|
149
|
+
n: n3, u: u3, v: v3,
|
|
150
|
+
hw: hwP, hh: hhP, hz: hzP,
|
|
151
|
+
// World-axis-aligned half-extents of the pitched plate (the broad-phase
|
|
152
|
+
// superset box — the ONE place these are derived).
|
|
153
|
+
aabb: {
|
|
154
|
+
hx: Math.abs(n3.x) * hwP + Math.abs(u3.x) * hhP + Math.abs(v3.x) * hzP,
|
|
155
|
+
hy: Math.abs(n3.y) * hwP + Math.abs(u3.y) * hhP + Math.abs(v3.y) * hzP,
|
|
156
|
+
hz: Math.abs(n3.z) * hwP + Math.abs(u3.z) * hhP + Math.abs(v3.z) * hzP
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Normalize to [0,360) so 315, −45, 675 all collapse to their canonical residue —
|
|
162
|
+
// the SAME normalization _buildReflectOps used (`((o % 360) + 360) % 360`), so the
|
|
163
|
+
// repoint is byte-identical.
|
|
164
|
+
var onorm = ((o % 360) + 360) % 360;
|
|
165
|
+
var mod90 = onorm % 90;
|
|
166
|
+
var isCardinal = (mod90 < EPS) || ((90 - mod90) < EPS);
|
|
167
|
+
|
|
168
|
+
// reflectBaseDeg = the drawn TALL-face line angle in the engine y-DOWN frame.
|
|
169
|
+
// Identical to the value the pre-repoint _buildReflectOps baked as `alphaScreen`.
|
|
170
|
+
var reflectBaseDeg = (90 + onorm) % 360;
|
|
171
|
+
|
|
172
|
+
var obb = null;
|
|
173
|
+
if (!isCardinal) {
|
|
174
|
+
var cx = (typeof spec.cx === 'number' && isFinite(spec.cx)) ? spec.cx : 0;
|
|
175
|
+
var cy = (typeof spec.cy === 'number' && isFinite(spec.cy)) ? spec.cy : 0;
|
|
176
|
+
var cz = (typeof spec.z === 'number' && isFinite(spec.z)) ? spec.z : 0;
|
|
177
|
+
var d = (typeof spec.d === 'number' && isFinite(spec.d) && spec.d > 0) ? spec.d : 0;
|
|
178
|
+
// theta = the slab's OWN rotation (matches the drawn rect rotated +orientationDeg).
|
|
179
|
+
// NOT reflectBaseDeg — the OBB is the geometric slab, the reflect base is the face
|
|
180
|
+
// LINE. Both are pure functions of the same onorm; neither is a second source.
|
|
181
|
+
var theta = onorm * DEG2RAD;
|
|
182
|
+
// Auxiliary face vectors (JS-side predicates / guards / tests — do NOT cross the
|
|
183
|
+
// ABI), in the ENGINE frame (y-DOWN screen coords — the frame frame.x/y/vx/vy and
|
|
184
|
+
// the reflect law's algebra live in):
|
|
185
|
+
// faceDir = (cos rb, sin rb) — the drawn long-face LINE direction. This is BOTH
|
|
186
|
+
// (a) the p5-drawn long axis (local (0,1) rotated +orientationDeg in screen
|
|
187
|
+
// coords: (−sin o, cos o) ≡ (cos(90+o), sin(90+o))), and
|
|
188
|
+
// (b) the +1-eigenvector (invariant line) of the reflect law R(reflectBaseDeg)
|
|
189
|
+
// = [cos2α sin2α; sin2α −cos2α] that `_buildReflectOps` bakes.
|
|
190
|
+
// normal = (−sin rb, cos rb) — faceDir rotated +90 in the same algebra: the
|
|
191
|
+
// −1-eigenvector R actually FLIPS, i.e. the crossing normal the tilted-reflect
|
|
192
|
+
// guard trigger tests against. Its overall SIGN is conventional (the guard is
|
|
193
|
+
// invariant under n → −n: flipping n flips both s(P) and v·n, and the guard
|
|
194
|
+
// only reads their products).
|
|
195
|
+
// One frame, one convention: drawn face ≡ reflect-law invariant line ≡ guard line.
|
|
196
|
+
// (A y-flipped variant here is the exact hand-mirrored-sign bug circulation §1 bans.)
|
|
197
|
+
var g = reflectBaseDeg * DEG2RAD;
|
|
198
|
+
var faceDir = { x: Math.cos(g), y: Math.sin(g) };
|
|
199
|
+
var normal = { x: -faceDir.y, y: faceDir.x };
|
|
200
|
+
obb = {
|
|
201
|
+
cx: cx, cy: cy, cz: cz,
|
|
202
|
+
hw: w / 2, hh: h / 2, hz: d / 2,
|
|
203
|
+
theta: theta,
|
|
204
|
+
faceDir: faceDir,
|
|
205
|
+
normal: normal
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { isCardinal: isCardinal, reflectBaseDeg: reflectBaseDeg, obb: obb };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Extract the CONVENTION-FREE, center-less engine `region` field from a resolved obb.
|
|
214
|
+
* The engine re-centers the LO's rect on its backing frame every tick, so the stored
|
|
215
|
+
* region carries NO center (a stored center would go stale) — only `{ kind, theta, hw,
|
|
216
|
+
* hh, hz }`. Returns `null` when there is no obb (cardinal ⇒ the AABB path). This is the
|
|
217
|
+
* ONE adapter from SlabFace geometry to the `LargeObject.region` schema field / ABI.
|
|
218
|
+
* @param {Object|null} obb — the `.obb` from resolveSlabFace (or null).
|
|
219
|
+
* @returns {{ kind: 'obb', theta: number, hw: number, hh: number, hz: number }|null}
|
|
220
|
+
*/
|
|
221
|
+
function regionFromObb(obb) {
|
|
222
|
+
if (!obb) { return null; }
|
|
223
|
+
return { kind: 'obb', theta: obb.theta, hw: obb.hw, hh: obb.hh, hz: obb.hz };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* BROAD-PHASE SUPERSET (CEO/architect MIRROR ruling, 2026-07-25 — the sugar-half
|
|
228
|
+
* broad-phase-rect flag closed here). The engine's spatial index is an AABB broad
|
|
229
|
+
* phase over the LO's registered rect (`octree.rs`); a tilted TALL slab's raw
|
|
230
|
+
* un-rotated rect is NARROWER in x than the rotated slab, so a beam aimed at the
|
|
231
|
+
* face TIP (outside the raw rect, inside the true rotated slab) is never a broad-
|
|
232
|
+
* phase candidate and never collides. FIX (superset discipline): the LO's broad-
|
|
233
|
+
* phase rect for a tilted wall = the ROTATED slab's AXIS-ALIGNED bounding box; the
|
|
234
|
+
* OBB `region` (narrow phase) is UNCHANGED and culls the AABB corners back out. This
|
|
235
|
+
* is the ONE place the rotated-AABB half-extents are derived (Wall never hand-derives
|
|
236
|
+
* them) — the SAME `theta` the region/reflect law already share, so no second
|
|
237
|
+
* convention. Returns half-extents (the LO rect stays center-symmetric about the
|
|
238
|
+
* slab center, which rotation preserves). z is NOT rotated by the xy-plane OBB, so
|
|
239
|
+
* the z half-extent passes through as `hz`.
|
|
240
|
+
* aabbHx = hw·|cos θ| + hh·|sin θ| (x reach of the rotated w×h slab)
|
|
241
|
+
* aabbHy = hw·|sin θ| + hh·|cos θ| (y reach)
|
|
242
|
+
* aabbHz = hz (z-slab depth is orientation-independent)
|
|
243
|
+
* @param {Object|null} obb — the `.obb` from resolveSlabFace (or null → null, the
|
|
244
|
+
* cardinal/AABB path: broad-phase rect stays the raw rect, byte-identical SU1).
|
|
245
|
+
* @returns {{ hx: number, hy: number, hz: number }|null}
|
|
246
|
+
*/
|
|
247
|
+
function aabbHalfExtentsFromObb(obb) {
|
|
248
|
+
if (!obb) { return null; }
|
|
249
|
+
var c = Math.abs(Math.cos(obb.theta));
|
|
250
|
+
var s = Math.abs(Math.sin(obb.theta));
|
|
251
|
+
return {
|
|
252
|
+
hx: obb.hw * c + obb.hh * s,
|
|
253
|
+
hy: obb.hw * s + obb.hh * c,
|
|
254
|
+
hz: obb.hz
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export { resolveSlabFace, regionFromObb, aabbHalfExtentsFromObb };
|