@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +87 -0
- package/LICENSE.md +664 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2920 -0
- package/dist/frqtl/objects/ObjectFactory.js +296 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +710 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +351 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
- package/dist/frqtl/objects/v1-replacements.js +29 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +34 -0
- package/dist/frqtl/parts/CollisionTypes.js +30 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
- package/dist/frqtl/parts/LargeObject.js +405 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +717 -0
- package/dist/frqtl/parts/OvenBox.js +611 -0
- package/dist/frqtl/parts/PointSource.js +762 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +211 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +690 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +325 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +671 -0
- package/dist/shim/notebook/knob.js +150 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +507 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +213 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +804 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +63 -4
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* valueNormalizer.js — the DECLARED-TYPE write gate for object properties.
|
|
5
|
+
*
|
|
6
|
+
* The sibling of `schema/index.js` inside the ONE descriptor owner
|
|
7
|
+
* (`lib/frqtl/objects/schema/`): index.js declares WHAT type each field is, this file
|
|
8
|
+
* states what that declaration MEANS at a write. Its single public entry point is
|
|
9
|
+
* re-exported as `ObjectSchema.normalizeValue` — consumers call it through ObjectSchema
|
|
10
|
+
* and never import this file directly, so there is exactly one public surface.
|
|
11
|
+
*
|
|
12
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
13
|
+
* normalizeValue(fieldDef, value, kind?) -> normalized value | THROW
|
|
14
|
+
* TYPES -> the row keys (asserted === FIELD_TYPES by
|
|
15
|
+
* schema/index.js at load time)
|
|
16
|
+
* RECT_TUPLE -> THE order of the rect write tuple, by length
|
|
17
|
+
* rectTupleText(n) -> that order as text, '[left, top, z, w, h, d]'
|
|
18
|
+
*
|
|
19
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
20
|
+
* None. A type gate on the write path — no engine state, no RNG draw. It REJECTS values
|
|
21
|
+
* the engine would otherwise have been handed as NaN geometry or a wrong wire type.
|
|
22
|
+
* Bench goldens cannot move (SU1).
|
|
23
|
+
*
|
|
24
|
+
* An ES module; objects/schema/index.js imports it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// ── Declared-type VALUE normalization (the write-time gate) ───────────────
|
|
29
|
+
//
|
|
30
|
+
// ONE entry point, exported by the descriptor owner:
|
|
31
|
+
//
|
|
32
|
+
// ObjectSchema.normalizeValue(fieldDef, value, kind?) → normalized value | THROW
|
|
33
|
+
//
|
|
34
|
+
// A field descriptor DECLARES a type; this is where that declaration is ENFORCED at
|
|
35
|
+
// the write. Throw-or-apply: the call either returns the value in the declared type's
|
|
36
|
+
// canonical form, or throws naming kind/field/expected type. Never a silent accept —
|
|
37
|
+
// a wrong-typed value that is merely STORED poisons every later reader (a non-array
|
|
38
|
+
// `color` reaching the renderer throws `s.color.join is not a function` on every
|
|
39
|
+
// frame, far from the write that caused it), which is the same failure class as a
|
|
40
|
+
// silently no-op write.
|
|
41
|
+
//
|
|
42
|
+
// Consumers FERRY this; they never re-implement a per-type predicate:
|
|
43
|
+
// - lib/frqtl/objects/FrqtlApp.js `updateObjectProperty` — the write gate,
|
|
44
|
+
// calls it ONCE after the readonly check (this is also where the `boolean` 0/1
|
|
45
|
+
// coercion lives now; it used to be hand-inlined there as the first row of this
|
|
46
|
+
// table).
|
|
47
|
+
// - lib/frqtl/objects/objects/makeControlsForObject.js `_rgbHandler` — the READ-side
|
|
48
|
+
// "is this a valid rgb value" probe.
|
|
49
|
+
//
|
|
50
|
+
// Rows are keyed by the ONE field-type vocabulary (lib/shim/field-descriptor's
|
|
51
|
+
// FIELD_TYPES); `schema/index.js` asserts the row keys ARE that list at load time, so a
|
|
52
|
+
// 13th type added at that owner without a row here FAILS LOUD at boot rather than
|
|
53
|
+
// silently passing an unvalidated value through.
|
|
54
|
+
//
|
|
55
|
+
// Per-type contract:
|
|
56
|
+
// number — a finite JS number. A numeric STRING is NOT a number (the caller
|
|
57
|
+
// parses). `constraint.integer` ROUNDS (canonical form, the same
|
|
58
|
+
// normalization spirit as boolean 0/1 → false/true);
|
|
59
|
+
// `constraint.min`/`max` are ENFORCED (out of the declared range
|
|
60
|
+
// throws — a clamp would silently store something the caller never
|
|
61
|
+
// asked for).
|
|
62
|
+
// boolean — true | false | 0 | 1, stored as the boolean (generic controls carry
|
|
63
|
+
// booleans as a 0/1 scalar, and every consumer tests IDENTITY: a stored
|
|
64
|
+
// 0 reads as "still visible"). Anything else throws — `'false'` is truthy
|
|
65
|
+
// and would silently store `true`.
|
|
66
|
+
// string — a JS string (empty allowed).
|
|
67
|
+
// enum — a declared member: a KEY or a VALUE of `constraint.values` (that map
|
|
68
|
+
// is identity for every enum but SlitScreen.detectAtSlits, whose
|
|
69
|
+
// values are the numeric wire codes). A field declaring NO vocabulary
|
|
70
|
+
// validates the primitive shape only (string|number).
|
|
71
|
+
// enum-set — an ARRAY whose every element is a declared member (copy returned).
|
|
72
|
+
// rgb — STRICTLY an array of exactly 3 finite numbers (copy returned). CSV
|
|
73
|
+
// and hex are NOT accepted: the CSV 'r,g,b,a' form is the frame-data
|
|
74
|
+
// `string_map['render.color']` WIRE format owned by lib/shim/frame-data,
|
|
75
|
+
// and parsing it here would mint a second interpreter of that wire
|
|
76
|
+
// outside its owner. A caller holding CSV/hex converts at the format
|
|
77
|
+
// owner BEFORE the write.
|
|
78
|
+
// rect — the WRITE wire form: an array of 4 or 6 finite numbers in the order
|
|
79
|
+
// RECT_TUPLE names (below). Not the `{left,top,w,h}` READ shape —
|
|
80
|
+
// `updateObjectProperty` indexes the value positionally.
|
|
81
|
+
// rect-array — an array of rect-shaped arrays (deep copy returned).
|
|
82
|
+
// readonly — every write throws (derived/computed: inspectable, not editable).
|
|
83
|
+
// forbidden — every write throws (engine-owned topology).
|
|
84
|
+
// object — any non-null object, arrays included (op-program bags, `center`
|
|
85
|
+
// [x,y,z], a `region`). Returned by REFERENCE — these bags are live.
|
|
86
|
+
// writable — passthrough: a Frame-kind ACCESS-LEVEL tag, not a value type (the
|
|
87
|
+
// actual type is inferred from the live read).
|
|
88
|
+
//
|
|
89
|
+
// `null`/`undefined` pass through UNCHANGED for every type except readonly/forbidden:
|
|
90
|
+
// "unset" is a DECLARED, type-agnostic value across this schema — `layer: null` means
|
|
91
|
+
// all layers, a null LIVE override (SphereEmitter.renderShape / renderColor) means
|
|
92
|
+
// "scheme default". It is a legitimate write, not a type error.
|
|
93
|
+
|
|
94
|
+
function _describeValue(v) {
|
|
95
|
+
if (v === null) { return 'null'; }
|
|
96
|
+
if (v === undefined) { return 'undefined'; }
|
|
97
|
+
if (Array.isArray(v)) { return 'array(length ' + v.length + ')'; }
|
|
98
|
+
return typeof v;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _previewValue(v) {
|
|
102
|
+
var s;
|
|
103
|
+
try { s = JSON.stringify(v); } catch (e) { s = String(v); }
|
|
104
|
+
if (s === undefined) { s = String(v); }
|
|
105
|
+
return (s.length > 60) ? (s.slice(0, 60) + '…') : s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _valueFail(kind, field, expected, value) {
|
|
109
|
+
throw new TypeError('[lib/frqtl/objects/schema] normalizeValue: ' +
|
|
110
|
+
(kind || '<kind>') + '.' + (field && field.name ? field.name : '<field>') +
|
|
111
|
+
' is declared type "' + (field && field.type) + '" and expects ' + expected +
|
|
112
|
+
'; got ' + _describeValue(value) + ' ' + _previewValue(value) +
|
|
113
|
+
'. (FAIL-LOUD — throw-or-apply, never a silently accepted wrong type.)');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _isFiniteNumber(v) { return typeof v === 'number' && isFinite(v); }
|
|
117
|
+
|
|
118
|
+
// ── THE rect write-tuple ORDER — one owner ────────────────────────────────
|
|
119
|
+
//
|
|
120
|
+
// The positional order of a `type:'rect'` write, keyed by tuple length. Every decode of a
|
|
121
|
+
// rect tuple (FrqtlApp.updateObjectProperty → Wall.setRect / LargeObject.setRect, the
|
|
122
|
+
// LargeObject constructor, LoDrawnCenter.rectAtCenter, worldRect below, and the engine's
|
|
123
|
+
// Rectangle::from_top_left(left, top, near, w, h, d) behind the WASM ABI) indexes in this
|
|
124
|
+
// order; `objects/test/rect-tuple-order.test.mjs` feeds each a tuple built BY NAME from this table and
|
|
125
|
+
// reads every field back by name. Every descriptor description that spells a tuple
|
|
126
|
+
// DERIVES its text from rectTupleText — never a hand-typed list.
|
|
127
|
+
var RECT_TUPLE = Object.freeze({
|
|
128
|
+
4: Object.freeze(['left', 'top', 'w', 'h']),
|
|
129
|
+
6: Object.freeze(['left', 'top', 'z', 'w', 'h', 'd'])
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** rectTupleText(n) → the order of the n-element rect tuple as text, e.g. '[left, top, z, w, h, d]'. */
|
|
133
|
+
function rectTupleText(n) {
|
|
134
|
+
if (!Object.prototype.hasOwnProperty.call(RECT_TUPLE, n)) {
|
|
135
|
+
throw new TypeError('[lib/frqtl/objects/schema] rectTupleText(' + _previewValue(n) +
|
|
136
|
+
'): a rect tuple has ' + Object.keys(RECT_TUPLE).join(' or ') + ' elements. (FAIL-LOUD.)');
|
|
137
|
+
}
|
|
138
|
+
return '[' + RECT_TUPLE[n].join(', ') + ']';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The declared members of an enum/enum-set field: the KEYS of constraint.values plus
|
|
143
|
+
* their VALUES (identical for every identity map; distinct for the numeric-wire-code
|
|
144
|
+
* map). Returns null when the field declares no vocabulary — the caller then validates
|
|
145
|
+
* the primitive shape only.
|
|
146
|
+
*/
|
|
147
|
+
function _enumMembers(field) {
|
|
148
|
+
var values = (field.constraint && field.constraint.values) || null;
|
|
149
|
+
if (!values || typeof values !== 'object') { return null; }
|
|
150
|
+
var keys = Object.keys(values);
|
|
151
|
+
if (keys.length === 0) { return null; }
|
|
152
|
+
var members = keys.slice();
|
|
153
|
+
for (var i = 0; i < keys.length; i++) {
|
|
154
|
+
if (members.indexOf(values[keys[i]]) === -1) { members.push(values[keys[i]]); }
|
|
155
|
+
}
|
|
156
|
+
return members;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _normalizeEnumMember(field, value, kind) {
|
|
160
|
+
var members = _enumMembers(field);
|
|
161
|
+
if (!members) {
|
|
162
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
163
|
+
_valueFail(kind, field, 'a string or number (this field declares no ' +
|
|
164
|
+
'constraint.values vocabulary to check against)', value);
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
if (members.indexOf(value) === -1) {
|
|
169
|
+
_valueFail(kind, field, 'one of [' + members.join(', ') + ']', value);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _normalizeRectTuple(field, value, kind, what) {
|
|
175
|
+
if (!Array.isArray(value) || (value.length !== 4 && value.length !== 6) ||
|
|
176
|
+
!value.every(_isFiniteNumber)) {
|
|
177
|
+
_valueFail(kind, field, what, value);
|
|
178
|
+
}
|
|
179
|
+
return value.slice();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* worldRect(r, where?) → { w, h[, left, top][, z, d] } — THE world-rect shape normalizer.
|
|
184
|
+
*
|
|
185
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────
|
|
186
|
+
* THROW-OR-APPLY on a world/canvas rect handed to a part or factory. Accepts the
|
|
187
|
+
* canonical `{left, top, w, h}` bag (a `Rectangle` is one, structurally) or the rect
|
|
188
|
+
* tuple this file already owns (RECT_TUPLE).
|
|
189
|
+
* THROWS when `r` is absent or `w`/`h` is missing, non-numeric or non-positive — a
|
|
190
|
+
* zero-high detector counts nothing and would report it as physics. `{width, height}`
|
|
191
|
+
* is not a rect spelling and throws like any other missing dimension. The origin
|
|
192
|
+
* (`left`/`top`) and the z pair are FERRIED when present and never defaulted.
|
|
193
|
+
*
|
|
194
|
+
* The world rect has ONE writer — the co-init call (`EGPTUniverse.create({rect})` ←
|
|
195
|
+
* `new FrqtlApp({canvasW, canvasH})` / `exp.createCanvas(w, h, cfg)`); this only reads.
|
|
196
|
+
*
|
|
197
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────
|
|
198
|
+
* None. No engine state, no RNG draw.
|
|
199
|
+
*
|
|
200
|
+
* @param {Object|Array} r
|
|
201
|
+
* @param {string} [where] — the reading site, quoted in the error.
|
|
202
|
+
* @returns {{w:number, h:number, left?:number, top?:number, z?:number, d?:number}}
|
|
203
|
+
*/
|
|
204
|
+
function worldRect(r, where) {
|
|
205
|
+
var site = where || 'worldRect(r)';
|
|
206
|
+
function fail(why) {
|
|
207
|
+
throw new TypeError('[lib/frqtl/objects/schema] ' + site + ': ' + why + '; got ' +
|
|
208
|
+
_describeValue(r) + ' ' + _previewValue(r) + '. A world rect is { left, top, w, h } ' +
|
|
209
|
+
'(or the ' + rectTupleText(4) + ' / ' + rectTupleText(6) + ' tuple) with positive w and h, ' +
|
|
210
|
+
'written ONCE by the co-init call — EGPTUniverse.create({ rect }) ← new FrqtlApp({ ' +
|
|
211
|
+
'canvasW, canvasH }) / exp.createCanvas(w, h, cfg). There is no default world size. ' +
|
|
212
|
+
'(FAIL-LOUD.)');
|
|
213
|
+
}
|
|
214
|
+
if (r === null || r === undefined || typeof r !== 'object') { fail('no world rect'); }
|
|
215
|
+
var out;
|
|
216
|
+
if (Array.isArray(r)) {
|
|
217
|
+
if ((r.length !== 4 && r.length !== 6) || !r.every(_isFiniteNumber)) {
|
|
218
|
+
fail('a rect tuple must be 4 or 6 finite numbers');
|
|
219
|
+
}
|
|
220
|
+
out = (r.length === 4)
|
|
221
|
+
? { left: r[0], top: r[1], w: r[2], h: r[3] }
|
|
222
|
+
: { left: r[0], top: r[1], z: r[2], w: r[3], h: r[4], d: r[5] };
|
|
223
|
+
} else {
|
|
224
|
+
out = { w: r.w, h: r.h };
|
|
225
|
+
['left', 'top', 'z', 'd'].forEach(function (k) {
|
|
226
|
+
if (r[k] === undefined || r[k] === null) { return; }
|
|
227
|
+
if (!_isFiniteNumber(r[k])) { fail('"' + k + '" must be a finite number'); }
|
|
228
|
+
out[k] = r[k];
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (!_isFiniteNumber(out.w) || !(out.w > 0)) { fail('w must be a positive finite number'); }
|
|
232
|
+
if (!_isFiniteNumber(out.h) || !(out.h > 0)) { fail('h must be a positive finite number'); }
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
var VALUE_NORMALIZERS = {
|
|
237
|
+
number: function (field, value, kind) {
|
|
238
|
+
if (!_isFiniteNumber(value)) {
|
|
239
|
+
_valueFail(kind, field, 'a finite number (a numeric STRING is not a ' +
|
|
240
|
+
'number — parse it at the caller)', value);
|
|
241
|
+
}
|
|
242
|
+
var c = field.constraint || {};
|
|
243
|
+
var out = (c.integer === true) ? Math.round(value) : value;
|
|
244
|
+
if (typeof c.min === 'number' && out < c.min) {
|
|
245
|
+
_valueFail(kind, field, 'a value >= the declared min ' + c.min, value);
|
|
246
|
+
}
|
|
247
|
+
if (typeof c.max === 'number' && out > c.max) {
|
|
248
|
+
_valueFail(kind, field, 'a value <= the declared max ' + c.max, value);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
},
|
|
252
|
+
boolean: function (field, value, kind) {
|
|
253
|
+
if (value === true || value === 1) { return true; }
|
|
254
|
+
if (value === false || value === 0) { return false; }
|
|
255
|
+
_valueFail(kind, field, 'true, false, 0 or 1', value);
|
|
256
|
+
},
|
|
257
|
+
string: function (field, value, kind) {
|
|
258
|
+
if (typeof value !== 'string') { _valueFail(kind, field, 'a string', value); }
|
|
259
|
+
return value;
|
|
260
|
+
},
|
|
261
|
+
'enum': function (field, value, kind) {
|
|
262
|
+
return _normalizeEnumMember(field, value, kind);
|
|
263
|
+
},
|
|
264
|
+
'enum-set': function (field, value, kind) {
|
|
265
|
+
if (!Array.isArray(value)) {
|
|
266
|
+
_valueFail(kind, field, 'an array of declared members', value);
|
|
267
|
+
}
|
|
268
|
+
for (var i = 0; i < value.length; i++) { _normalizeEnumMember(field, value[i], kind); }
|
|
269
|
+
return value.slice();
|
|
270
|
+
},
|
|
271
|
+
rgb: function (field, value, kind) {
|
|
272
|
+
if (!Array.isArray(value) || value.length !== 3 || !value.every(_isFiniteNumber)) {
|
|
273
|
+
_valueFail(kind, field, 'an array of exactly 3 finite numbers [r, g, b] ' +
|
|
274
|
+
'(a CSV "r,g,b,a" string is the frame-data render wire, not this ' +
|
|
275
|
+
'field — convert at lib/shim/frame-data before the write)', value);
|
|
276
|
+
}
|
|
277
|
+
return value.slice();
|
|
278
|
+
},
|
|
279
|
+
rect: function (field, value, kind) {
|
|
280
|
+
return _normalizeRectTuple(field, value, kind,
|
|
281
|
+
'an array of 4 finite numbers ' + rectTupleText(4) + ' or the 6-element ' +
|
|
282
|
+
'explicit z/depth form ' + rectTupleText(6));
|
|
283
|
+
},
|
|
284
|
+
'rect-array': function (field, value, kind) {
|
|
285
|
+
if (!Array.isArray(value)) {
|
|
286
|
+
_valueFail(kind, field, 'an array of rect tuples', value);
|
|
287
|
+
}
|
|
288
|
+
var out = [];
|
|
289
|
+
for (var i = 0; i < value.length; i++) {
|
|
290
|
+
out.push(_normalizeRectTuple(field, value[i], kind,
|
|
291
|
+
'every entry to be an array of 4 finite numbers ' + rectTupleText(4) +
|
|
292
|
+
' (or 6, ' + rectTupleText(6) + '); entry ' + i + ' is not'));
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
},
|
|
296
|
+
readonly: function (field, value, kind) {
|
|
297
|
+
_valueFail(kind, field, 'NO write at all — a readonly field is derived/' +
|
|
298
|
+
'computed and inspectable only', value);
|
|
299
|
+
},
|
|
300
|
+
forbidden: function (field, value, kind) {
|
|
301
|
+
_valueFail(kind, field, 'NO write at all — this property is engine-owned ' +
|
|
302
|
+
'topology', value);
|
|
303
|
+
},
|
|
304
|
+
object: function (field, value, kind) {
|
|
305
|
+
if (typeof value !== 'object') {
|
|
306
|
+
_valueFail(kind, field, 'a structured object (arrays included)', value);
|
|
307
|
+
}
|
|
308
|
+
return value; // by REFERENCE — op-program / region / center bags are live
|
|
309
|
+
},
|
|
310
|
+
writable: function (field, value) { return value; }
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* normalizeValue(fieldDef, value, kind) → the value in its declared canonical form.
|
|
315
|
+
*
|
|
316
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────
|
|
317
|
+
* THROW-OR-APPLY. `fieldDef` is a descriptor field object (`ObjectSchema.get(kind)
|
|
318
|
+
* .fields[i]`); `kind` is optional context that only sharpens the error message.
|
|
319
|
+
* Throws a TypeError naming kind/field/declared-type/expected-form when the value
|
|
320
|
+
* cannot be accepted. See the per-type contract table above.
|
|
321
|
+
*
|
|
322
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────
|
|
323
|
+
* None. This is a type gate on the write path — it changes no engine state, draws no
|
|
324
|
+
* RNG, and rejects values the engine would otherwise have been handed as NaN
|
|
325
|
+
* geometry or a wrong wire type. Bench goldens cannot move (SU1).
|
|
326
|
+
*
|
|
327
|
+
* @param {{name:string, type:string, constraint?:object}} fieldDef
|
|
328
|
+
* @param {*} value
|
|
329
|
+
* @param {string} [kind] — the object kind, for the error message.
|
|
330
|
+
* @returns {*} the normalized value.
|
|
331
|
+
* @throws {TypeError} FAIL-LOUD on a wrong type / undeclared enum member / declared-
|
|
332
|
+
* constraint violation / any write to a readonly or forbidden field.
|
|
333
|
+
*/
|
|
334
|
+
function normalizeValue(fieldDef, value, kind) {
|
|
335
|
+
if (!fieldDef || typeof fieldDef !== 'object' || typeof fieldDef.type !== 'string') {
|
|
336
|
+
throw new TypeError('[lib/frqtl/objects/schema] normalizeValue requires a field ' +
|
|
337
|
+
'descriptor carrying a "type"; got ' + _describeValue(fieldDef) +
|
|
338
|
+
'. (FAIL-LOUD.)');
|
|
339
|
+
}
|
|
340
|
+
if (!Object.prototype.hasOwnProperty.call(VALUE_NORMALIZERS, fieldDef.type)) {
|
|
341
|
+
throw new TypeError('[lib/frqtl/objects/schema] normalizeValue: field "' +
|
|
342
|
+
fieldDef.name + '" declares type "' + fieldDef.type + '", which has no ' +
|
|
343
|
+
'normalizer row. Field types are owned by lib/shim/field-descriptor ' +
|
|
344
|
+
'(FIELD_TYPES) — add the type THERE and a row in VALUE_NORMALIZERS here. ' +
|
|
345
|
+
'(FAIL-LOUD.)');
|
|
346
|
+
}
|
|
347
|
+
var row = VALUE_NORMALIZERS[fieldDef.type];
|
|
348
|
+
// readonly/forbidden reject EVERY write, the unset sentinel included.
|
|
349
|
+
if (fieldDef.type === 'readonly' || fieldDef.type === 'forbidden') {
|
|
350
|
+
return row(fieldDef, value, kind);
|
|
351
|
+
}
|
|
352
|
+
// The unset sentinel — a DECLARED, type-agnostic value here (layer:null = all
|
|
353
|
+
// layers; a null LIVE override = scheme default). A legitimate write, not a type
|
|
354
|
+
// error.
|
|
355
|
+
if (value === null || value === undefined) { return value; }
|
|
356
|
+
return row(fieldDef, value, kind);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
// The row keys — schema/index.js asserts these are EXACTLY lib/shim/field-descriptor's FIELD_TYPES at
|
|
362
|
+
// load time (the type vocabulary has ONE owner; this is the drift gate, never a second vocabulary).
|
|
363
|
+
const TYPES = Object.freeze(Object.keys(VALUE_NORMALIZERS));
|
|
364
|
+
|
|
365
|
+
// RECT_TUPLE + rectTupleText: THE rect write-tuple order and its text form.
|
|
366
|
+
export { normalizeValue, worldRect, RECT_TUPLE, rectTupleText, TYPES };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* _collisionMatrixLanes — the ONE lane table the collision-matrix experiments sweep.
|
|
5
|
+
*
|
|
6
|
+
* The collision matrix is a DENSITY sweep. Density — `ρ = mass / capacity` — is the
|
|
7
|
+
* physical quantity the experiment reads: the per-tick exchange ceiling of
|
|
8
|
+
* `voxel_collision_pass` carries occupancy (ρ_A·ρ_B), so bounce TIMING emerges from
|
|
9
|
+
* density. Dense lanes complete the two-bucket exchange earliest; sparse lanes linger
|
|
10
|
+
* or pass through.
|
|
11
|
+
*
|
|
12
|
+
* The lanes are therefore declared as DENSITIES, and the absolute mass is derived from
|
|
13
|
+
* the capacity the ENGINE reports for the body it actually built. A hardcoded mass list
|
|
14
|
+
* is only correct for one capacity: `[1,8,64,512,1024,2048,4096]` spans ρ 0→1 for a 2D
|
|
15
|
+
* 64x64 voxel (capacity 4096) and collapses to ρ ≤ 0.0156 for the 3D 64x64x64 voxel
|
|
16
|
+
* (capacity 262144), which squeezes the whole sweep into its sparse corner and leaves
|
|
17
|
+
* the 3D scene with no dense lanes at all.
|
|
18
|
+
*
|
|
19
|
+
* The powers-of-two ladder is deliberate: collision probability goes as density², so a
|
|
20
|
+
* LOG scale is what reads cleanly across the transition.
|
|
21
|
+
*
|
|
22
|
+
* @module _collisionMatrixLanes
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lane densities, sparse → full. `2^-12 … 2^0`.
|
|
27
|
+
*
|
|
28
|
+
* Against a 2D capacity of 4096 these reproduce the canonical mass ladder exactly:
|
|
29
|
+
* 1, 8, 64, 512, 1024, 2048, 4096.
|
|
30
|
+
*
|
|
31
|
+
* @type {Array<number>}
|
|
32
|
+
*/
|
|
33
|
+
var LANE_DENSITIES = [
|
|
34
|
+
Math.pow(2, -12), Math.pow(2, -9), Math.pow(2, -6), Math.pow(2, -3),
|
|
35
|
+
Math.pow(2, -2), Math.pow(2, -1), 1
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the lane densities against a real capacity.
|
|
40
|
+
*
|
|
41
|
+
* @param {number} capacity - The body capacity the ENGINE reports (`mass` of a freshly
|
|
42
|
+
* built full body). 2^(2·level) in 2D, 2^(3·level) in 3D.
|
|
43
|
+
* @returns {Array<number>} one integer mass per lane, sparse → full, each ≥ 1 and
|
|
44
|
+
* clamped to `capacity`.
|
|
45
|
+
* @throws {Error} FAIL-LOUD when capacity is not a positive finite number — a lane
|
|
46
|
+
* ladder built against a bad capacity is the exact silent-wrong-scene this module
|
|
47
|
+
* exists to prevent.
|
|
48
|
+
*/
|
|
49
|
+
function laneMassesForCapacity(capacity) {
|
|
50
|
+
if (!isFinite(capacity) || capacity <= 0) {
|
|
51
|
+
throw new Error('[collisionMatrixLanes] capacity must be a positive number, got ' +
|
|
52
|
+
JSON.stringify(capacity) + '. Read it back from the engine after building a ' +
|
|
53
|
+
'body (universe.getFrameProperty(qid, \'mass\')) — never assume it.');
|
|
54
|
+
}
|
|
55
|
+
return LANE_DENSITIES.map(function (rho) {
|
|
56
|
+
return Math.max(1, Math.min(capacity, Math.round(rho * capacity)));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var api = {
|
|
61
|
+
LANE_DENSITIES: LANE_DENSITIES,
|
|
62
|
+
laneMassesForCapacity: laneMassesForCapacity
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { api as collisionMatrixLanes };
|
|
66
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* _setupOptions — the ONE unknown-option-key validator for every `objects/setup/`
|
|
5
|
+
* factory.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this exists
|
|
8
|
+
*
|
|
9
|
+
* Every setup factory reads its bag with the idiom
|
|
10
|
+
* `options.X !== undefined ? options.X : default` — 56 occurrences across 16 files,
|
|
11
|
+
* and NONE of them validated the key set. A misspelling therefore resolved to the
|
|
12
|
+
* DEFAULT and ran silently: a blackbody factory handed `{ temperatuer: 5000 }` ran a
|
|
13
|
+
* 300-quanta cavity, the badge stayed `ok`, and nothing anywhere said a word. This is the surface
|
|
14
|
+
* the notebooks' own markdown invites a reader to edit, so the failure mode is "you got
|
|
15
|
+
* a different experiment than the one you asked for, and you were never told."
|
|
16
|
+
*
|
|
17
|
+
* `EGPTUniverse.create()` and `EGPTUniverse.init()` already close exactly this hole one
|
|
18
|
+
* layer down (`wasm-engine.js` `_assertNoUnknownKeys`). This is the same rule at the
|
|
19
|
+
* factory layer, and it FAILS LOUD — no warn-path, no lenient mode, no opt-out flag.
|
|
20
|
+
*
|
|
21
|
+
* ## The contract, and who owns it
|
|
22
|
+
*
|
|
23
|
+
* A factory OWNS its option vocabulary, so the key list is declared in the factory's own
|
|
24
|
+
* file (as `setupX.OPTIONS`) beside the code that reads those keys — never re-enumerated
|
|
25
|
+
* here, and never copied from another module's schema. This module owns only the CHECK.
|
|
26
|
+
*
|
|
27
|
+
* Two consequences:
|
|
28
|
+
* 1. A factory that ferries its bag onward composes its list from the downstream
|
|
29
|
+
* owner's EXPORTED list rather than retyping it — e.g. `setupTunnelPointExit`
|
|
30
|
+
* hands an augmented bag to `setupSphereEmitter3D`, so its legal set is
|
|
31
|
+
* `OWN.concat(setupSphereEmitter3D.OPTIONS)`. Copying the downstream keys by hand
|
|
32
|
+
* would be the schema-mirror bug this workstream exists to remove.
|
|
33
|
+
* 2. `setup-factory-option-vocabulary.test.mjs` re-derives every factory's ACTUAL
|
|
34
|
+
* read-set by static trace and asserts the declared list covers it, so a key added
|
|
35
|
+
* to the code and forgotten in the list is a CI failure, not a caller's bad day.
|
|
36
|
+
*
|
|
37
|
+
* The message names every legal key, and — because the whole point is a typo — it leads
|
|
38
|
+
* with the nearest legal key when one is within edit distance 2.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { ObjectSchema } from '../schema/index.js';
|
|
42
|
+
|
|
43
|
+
/** Levenshtein, capped: we only care about "is this within 2 edits". */
|
|
44
|
+
function _editDistance(a, b) {
|
|
45
|
+
if (Math.abs(a.length - b.length) > 2) return 99;
|
|
46
|
+
var prev = [], cur = [], i, j;
|
|
47
|
+
for (j = 0; j <= b.length; j++) prev[j] = j;
|
|
48
|
+
for (i = 1; i <= a.length; i++) {
|
|
49
|
+
cur[0] = i;
|
|
50
|
+
for (j = 1; j <= b.length; j++) {
|
|
51
|
+
cur[j] = Math.min(
|
|
52
|
+
prev[j] + 1,
|
|
53
|
+
cur[j - 1] + 1,
|
|
54
|
+
prev[j - 1] + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
prev = cur.slice();
|
|
58
|
+
}
|
|
59
|
+
return prev[b.length];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Nearest legal key within 2 edits (case-insensitive), or null. */
|
|
63
|
+
function _suggest(key, legalKeys) {
|
|
64
|
+
var best = null, bestD = 3, lk = key.toLowerCase();
|
|
65
|
+
for (var i = 0; i < legalKeys.length; i++) {
|
|
66
|
+
var d = _editDistance(lk, String(legalKeys[i]).toLowerCase());
|
|
67
|
+
if (d < bestD) { bestD = d; best = legalKeys[i]; }
|
|
68
|
+
}
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Throw unless every own key of `options` is declared in `legalKeys`.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} factoryName - e.g. 'setupDoubleSlit'. Named in the error.
|
|
76
|
+
* @param {Object} [options] - the caller's bag. null/undefined is legal (no options).
|
|
77
|
+
* @param {string[]} legalKeys - the factory's OWN exported vocabulary (`setupX.OPTIONS`).
|
|
78
|
+
* @returns {Object} the same `options` (or `{}`), so a factory can write
|
|
79
|
+
* `options = assertSetupOptions('setupX', options, OPTIONS);`
|
|
80
|
+
*/
|
|
81
|
+
function assertSetupOptions(factoryName, options, legalKeys) {
|
|
82
|
+
if (options === undefined || options === null) return {};
|
|
83
|
+
if (typeof options !== 'object' || Array.isArray(options)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'[' + factoryName + '] options must be a plain object, got ' +
|
|
86
|
+
(Array.isArray(options) ? 'an array' : typeof options) + '. ' +
|
|
87
|
+
'Legal keys: ' + legalKeys.slice().sort().join(', ') + '.'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
var sorted = legalKeys.slice().sort();
|
|
91
|
+
var keys = Object.keys(options);
|
|
92
|
+
for (var i = 0; i < keys.length; i++) {
|
|
93
|
+
if (legalKeys.indexOf(keys[i]) !== -1) continue;
|
|
94
|
+
var hint = _suggest(keys[i], legalKeys);
|
|
95
|
+
throw new Error(
|
|
96
|
+
'[' + factoryName + '] unknown option \'' + keys[i] + '\'' +
|
|
97
|
+
(hint ? ' — did you mean \'' + hint + '\'?' : '') +
|
|
98
|
+
' This factory reads NONE of the keys it does not declare, so an ' +
|
|
99
|
+
'unrecognized one would have silently resolved to a default and run a ' +
|
|
100
|
+
'DIFFERENT experiment than you asked for. Legal options: ' +
|
|
101
|
+
sorted.join(', ') + '.'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* requireWorldRect(where, rect) → `{ w, h, … }` — the world rect a factory builds in,
|
|
109
|
+
* through the ONE normalizer (`ObjectSchema.worldRect`). A factory never supplies a size:
|
|
110
|
+
* it hands `universe.universe_rect`, which the ONE construction authority set from the
|
|
111
|
+
* co-init call. Absent or malformed THROWS naming the co-init — never a default.
|
|
112
|
+
*/
|
|
113
|
+
function requireWorldRect(where, rect) {
|
|
114
|
+
return ObjectSchema.worldRect(rect, where);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { assertSetupOptions, requireWorldRect };
|
|
118
|
+
|