@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,14 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* kinds-entry.js — the published door to the kind registry: `@descix/frqtl-web/kinds` and
|
|
5
|
+
* `@descix/frqtl-node/kinds`. Exactly the four names below, re-exported from THE registry (kinds.js — the
|
|
6
|
+
* same module instance every part registers in); the rest of kinds.js stays internal.
|
|
7
|
+
*
|
|
8
|
+
* KINDS_EXPORTS the published names (the parity gate holds both packages' ./kinds to this table)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The names `./kinds` publishes. */
|
|
12
|
+
export const KINDS_EXPORTS = Object.freeze(['registerKind', 'kindNamed', 'kindOf', 'registeredKinds']);
|
|
13
|
+
|
|
14
|
+
export { registerKind, kindNamed, kindOf, registeredKinds } from './kinds.js';
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* kinds.js — THE kind registry: a kind registers when the module that defines it is imported.
|
|
5
|
+
*
|
|
6
|
+
* registerKind(Class, { from, descriptor, defaults?, build?, palette? })
|
|
7
|
+
* The kind's NAME IS `Class.name` — there is no name field (a `name` key is refused: it would
|
|
8
|
+
* be a second derivation of the same fact). Fork = copy the part file, rename the class,
|
|
9
|
+
* import it: the copy registers under its new name with no other edit.
|
|
10
|
+
* from REQUIRED — the defining module's own URL (`import.meta.url`), so a copied
|
|
11
|
+
* file carries the right value untouched; a duplicate registration names both
|
|
12
|
+
* descriptor REQUIRED — the editable-field contract, validated by schema/'s own validator
|
|
13
|
+
* (schema/validate-descriptor.js). It carries no `kind`: the registry stamps
|
|
14
|
+
* `descriptor.kind = Class.name`. A COMPOSED FORK passes its parent's registered
|
|
15
|
+
* descriptor (kindOf(Parent).descriptor): the registry registers a copy of it,
|
|
16
|
+
* stamped with the fork's own name — the parent's row is never touched
|
|
17
|
+
* defaults the construction defaults ObjectFactory merges under a config (ObjectSchema.defaultPropsFor)
|
|
18
|
+
* build (props, ctx, Class) → the declared part — ObjectFactory.create's per-kind step; Class is the
|
|
19
|
+
* class that registered (a composed fork passing its parent's build gets an instance of the
|
|
20
|
+
* FORK, stamped `_schemaKind = Class.name`). A kind
|
|
21
|
+
* with no build registers a descriptor only: ObjectFactory refuses to build it,
|
|
22
|
+
* naming the kind's own construction door
|
|
23
|
+
* palette { displayName, category, iconHint, rank } — present ⇔ STUDIO can DROP the kind
|
|
24
|
+
* (ObjectSchema.placeableCatalog, ordered by rank; equal ranks keep registration
|
|
25
|
+
* order, so a fork — a renamed copy — sits beside its original)
|
|
26
|
+
* THROWS on an anonymous class, a malformed row, a name a scope descriptor already owns
|
|
27
|
+
* (schema/scopes.js) unless the descriptor IS that scope row's object, or a second
|
|
28
|
+
* registration of the same name.
|
|
29
|
+
* kindNamed(name) → Class THROWS when no module defining it was imported in this realm
|
|
30
|
+
* kindOf(ClassOrName) → row|null { name, Class, from, descriptor, defaults, build, palette } — the row and
|
|
31
|
+
* its descriptor/defaults/palette are FROZEN (deeply): a fork reads its parent's
|
|
32
|
+
* row and cannot change it
|
|
33
|
+
* registeredKinds() → string[] in registration order
|
|
34
|
+
* placeableKinds() → rows with a palette, by rank (stable: equal ranks in registration order)
|
|
35
|
+
*
|
|
36
|
+
* A registry is an IMPORT fact: a realm has the kinds it imported. The boot imports the parts barrel
|
|
37
|
+
* (@descix/frqtl-web/parts), so every booted realm has every published part; a kind defined outside
|
|
38
|
+
* the parts directory registers when the module that uses it imports it. This file imports nothing
|
|
39
|
+
* of the parts, so the arrow runs part → registry, never back.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { validateDescriptor } from './schema/validate-descriptor.js';
|
|
43
|
+
import { SCOPE_DESCRIPTORS } from './schema/scopes.js';
|
|
44
|
+
|
|
45
|
+
/** The keys a registration row may carry. */
|
|
46
|
+
export const KIND_ROW_KEYS = Object.freeze(['from', 'descriptor', 'defaults', 'build', 'palette']);
|
|
47
|
+
/** The keys a palette entry carries. */
|
|
48
|
+
export const PALETTE_KEYS = Object.freeze(['displayName', 'category', 'iconHint', 'rank']);
|
|
49
|
+
|
|
50
|
+
const _rows = new Map(); // name → frozen row
|
|
51
|
+
|
|
52
|
+
function _fail(msg) {
|
|
53
|
+
throw new Error('[kinds] ' + msg + ' (FAIL-LOUD.)');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {Function} Class — the part's class (its name is the kind name)
|
|
58
|
+
* @param {{from: string, descriptor: object, defaults?: object, build?: Function, palette?: object}} row
|
|
59
|
+
* @returns {Function} Class
|
|
60
|
+
*/
|
|
61
|
+
export function registerKind(Class, row) {
|
|
62
|
+
if (typeof Class !== 'function') { _fail('registerKind(Class, row): Class must be the part\'s class (got ' + typeof Class + ').'); }
|
|
63
|
+
const name = Class.name;
|
|
64
|
+
if (!name) { _fail('registerKind: the class is anonymous — the kind name IS the class name, so a kind\'s class must be named.'); }
|
|
65
|
+
if (!row || typeof row !== 'object') { _fail('registerKind(' + name + ', row): row must be an object {' + KIND_ROW_KEYS.join(', ') + '}.'); }
|
|
66
|
+
if (Object.prototype.hasOwnProperty.call(row, 'name')) {
|
|
67
|
+
_fail('registerKind(' + name + '): a row carries no `name` — the kind name IS the class name (' + name + '). ' +
|
|
68
|
+
'To register under another name, rename the class.');
|
|
69
|
+
}
|
|
70
|
+
for (const k of Object.keys(row)) {
|
|
71
|
+
if (KIND_ROW_KEYS.indexOf(k) === -1) { _fail('registerKind(' + name + '): unknown row key "' + k + '" — a row carries ' + KIND_ROW_KEYS.join(', ') + '.'); }
|
|
72
|
+
}
|
|
73
|
+
if (typeof row.from !== 'string' || !row.from) {
|
|
74
|
+
_fail('registerKind(' + name + '): `from` is required — the defining module\'s own URL (from: import.meta.url).');
|
|
75
|
+
}
|
|
76
|
+
try { new URL(row.from); } catch (e) { _fail('registerKind(' + name + '): `from` must be a URL (import.meta.url), got ' + JSON.stringify(row.from) + '.'); }
|
|
77
|
+
// A scope row's name is refused — except to a kind whose descriptor IS that row's object (the
|
|
78
|
+
// LargeObject part registers the engine door's row by reference: still one owner, schema/scopes.js).
|
|
79
|
+
const scopeRow = Object.prototype.hasOwnProperty.call(SCOPE_DESCRIPTORS, name) ? SCOPE_DESCRIPTORS[name] : null;
|
|
80
|
+
if (scopeRow && row.descriptor !== scopeRow) {
|
|
81
|
+
_fail('registerKind(' + name + ', …): "' + name + '" is a SCOPE descriptor row (schema/scopes.js SCOPE_DESCRIPTORS) — ' +
|
|
82
|
+
'a scope is not a kind, and one name has one owner. Rename the class, or register the scope row ' +
|
|
83
|
+
'itself by reference (descriptor: SCOPE_DESCRIPTORS.' + name + ') (' + row.from + ').');
|
|
84
|
+
}
|
|
85
|
+
if (_rows.has(name)) {
|
|
86
|
+
_fail('kind "' + name + '" is already registered by ' + _rows.get(name).from + '; ' + row.from +
|
|
87
|
+
' registers it again — rename the class in the copy.');
|
|
88
|
+
}
|
|
89
|
+
let descriptor = row.descriptor;
|
|
90
|
+
if (!descriptor || typeof descriptor !== 'object') { _fail('registerKind(' + name + '): `descriptor` is required.'); }
|
|
91
|
+
// A composed fork inherits a REGISTERED kind's descriptor: register a copy, stamped with the fork's name.
|
|
92
|
+
if (!scopeRow && _isRegisteredDescriptor(descriptor)) {
|
|
93
|
+
const copy = {};
|
|
94
|
+
for (const k of Object.keys(descriptor)) { if (k !== 'kind') { copy[k] = descriptor[k]; } }
|
|
95
|
+
descriptor = copy;
|
|
96
|
+
}
|
|
97
|
+
if (!scopeRow && Object.prototype.hasOwnProperty.call(descriptor, 'kind')) {
|
|
98
|
+
_fail('registerKind(' + name + '): the descriptor carries a `kind` — the registry stamps it from the class ' +
|
|
99
|
+
'name; a typed copy is a second derivation of the same fact. Delete the descriptor\'s kind line.');
|
|
100
|
+
}
|
|
101
|
+
validateDescriptor(name, descriptor);
|
|
102
|
+
if (row.defaults !== undefined && (row.defaults === null || typeof row.defaults !== 'object' || Array.isArray(row.defaults))) {
|
|
103
|
+
_fail('registerKind(' + name + '): `defaults` must be a props object when given.');
|
|
104
|
+
}
|
|
105
|
+
if (row.build !== undefined && typeof row.build !== 'function') {
|
|
106
|
+
_fail('registerKind(' + name + '): `build` must be a function (props, ctx) → part when given.');
|
|
107
|
+
}
|
|
108
|
+
if (row.palette !== undefined) {
|
|
109
|
+
const p = row.palette;
|
|
110
|
+
if (!p || typeof p !== 'object') { _fail('registerKind(' + name + '): `palette` must be {' + PALETTE_KEYS.join(', ') + '}.'); }
|
|
111
|
+
for (const k of Object.keys(p)) {
|
|
112
|
+
if (PALETTE_KEYS.indexOf(k) === -1) { _fail('registerKind(' + name + '): unknown palette key "' + k + '" — ' + PALETTE_KEYS.join(', ') + '.'); }
|
|
113
|
+
}
|
|
114
|
+
for (const k of ['displayName', 'category', 'iconHint']) {
|
|
115
|
+
if (typeof p[k] !== 'string' || !p[k]) { _fail('registerKind(' + name + '): palette.' + k + ' must be a non-empty string.'); }
|
|
116
|
+
}
|
|
117
|
+
if (typeof p.rank !== 'number' || !Number.isInteger(p.rank) || p.rank < 0) {
|
|
118
|
+
_fail('registerKind(' + name + '): palette.rank must be a non-negative integer (the palette order).');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!scopeRow) { // a scope row carries its own kind (= its name)
|
|
122
|
+
Object.defineProperty(descriptor, 'kind', { value: name, enumerable: true, writable: false, configurable: false });
|
|
123
|
+
}
|
|
124
|
+
_rows.set(name, Object.freeze({
|
|
125
|
+
name: name,
|
|
126
|
+
Class: Class,
|
|
127
|
+
from: row.from,
|
|
128
|
+
descriptor: _deepFreeze(descriptor),
|
|
129
|
+
defaults: row.defaults === undefined ? undefined : _deepFreeze(Object.assign({}, row.defaults)),
|
|
130
|
+
build: row.build,
|
|
131
|
+
palette: row.palette ? Object.freeze(Object.assign({}, row.palette)) : undefined
|
|
132
|
+
}));
|
|
133
|
+
return Class;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Is `d` the descriptor object of a kind already registered (kindOf(Parent).descriptor)? */
|
|
137
|
+
function _isRegisteredDescriptor(d) {
|
|
138
|
+
for (const r of _rows.values()) { if (r.descriptor === d) { return true; } }
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Freeze `o` and every plain object / array under it (functions and class instances are left as they are). */
|
|
143
|
+
function _deepFreeze(o) {
|
|
144
|
+
if (!o || typeof o !== 'object' || Object.isFrozen(o)) { return o; }
|
|
145
|
+
const proto = Object.getPrototypeOf(o);
|
|
146
|
+
if (proto !== Object.prototype && proto !== null && !Array.isArray(o)) { return o; }
|
|
147
|
+
Object.freeze(o);
|
|
148
|
+
for (const k of Object.keys(o)) { _deepFreeze(o[k]); }
|
|
149
|
+
return o;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The refusal an unregistered kind name reads — the import form, never a path. */
|
|
153
|
+
export function unregisteredKindMessage(name) {
|
|
154
|
+
return 'kind "' + name + '" is not registered in this realm — import the module that defines it; ' +
|
|
155
|
+
'published parts: \'@descix/frqtl-web/parts\'';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} name
|
|
160
|
+
* @returns {Function} the registered class. THROWS when no module defining it was imported here.
|
|
161
|
+
*/
|
|
162
|
+
export function kindNamed(name) {
|
|
163
|
+
const row = _rows.get(name);
|
|
164
|
+
if (!row) { throw new Error('[kinds] ' + unregisteredKindMessage(name) + '. (FAIL-LOUD.)'); }
|
|
165
|
+
return row.Class;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {Function|string} classOrName
|
|
170
|
+
* @returns {object|null} the registration row
|
|
171
|
+
*/
|
|
172
|
+
export function kindOf(classOrName) {
|
|
173
|
+
if (typeof classOrName === 'string') { return _rows.get(classOrName) || null; }
|
|
174
|
+
if (typeof classOrName === 'function') {
|
|
175
|
+
const row = _rows.get(classOrName.name);
|
|
176
|
+
return (row && row.Class === classOrName) ? row : null;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** @returns {string[]} the registered kind names, in registration order */
|
|
182
|
+
export function registeredKinds() {
|
|
183
|
+
return Array.from(_rows.keys());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** @returns {object[]} the rows STUDIO can drop, ordered by palette.rank (a stable sort: ties keep registration order) */
|
|
187
|
+
export function placeableKinds() {
|
|
188
|
+
return Array.from(_rows.values()).filter(function (r) { return !!r.palette; })
|
|
189
|
+
.sort(function (a, b) { return a.palette.rank - b.palette.rank; });
|
|
190
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Box — Engine-agnostic rectangular enclosure made of 4 LargeObject walls.
|
|
5
|
+
* Creates a rectangular enclosure with configurable wall thickness, collision type,
|
|
6
|
+
* and an optional slit (gap) on any one wall. Used by OvenBox (blackbody) and
|
|
7
|
+
* SlitScreen (double-slit) as the physical boundary primitive.
|
|
8
|
+
*
|
|
9
|
+
* Engine-agnostic: works with both the JS reference engine and the Rust/WASM engine.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LargeObject } from '../../parts/LargeObject.js';
|
|
13
|
+
import { CollisionTypes } from '../../parts/CollisionTypes.js';
|
|
14
|
+
import { CollisionActions } from '../../parts/CollisionActions.js';
|
|
15
|
+
import { registerKind } from '../kinds.js';
|
|
16
|
+
|
|
17
|
+
// ── Imports ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
// ── Box ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rectangular enclosure made of 4 LargeObject walls.
|
|
23
|
+
*
|
|
24
|
+
* Walls are positioned so their outer edges align with the **outer** rect
|
|
25
|
+
* (`left - wallThickness`, `top - wallThickness`, …). The caller supplies the
|
|
26
|
+
* inner rect; walls extend outward. A slit on one wall is implemented as two
|
|
27
|
+
* LargeObject segments (top and bottom) leaving a gap of `slit.height` pixels
|
|
28
|
+
* in the center.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} universe - The engine universe instance (JS or WASM).
|
|
31
|
+
* @param {Object} options - Configuration.
|
|
32
|
+
* @param {number} options.left - Inner box left X (physics pixels).
|
|
33
|
+
* @param {number} options.top - Inner box top Y (physics pixels).
|
|
34
|
+
* @param {number} options.width - Inner box width (physics pixels).
|
|
35
|
+
* @param {number} options.height - Inner box height (physics pixels).
|
|
36
|
+
* @param {number} [options.wallThickness=10] - Wall thickness in pixels. Walls
|
|
37
|
+
* grow outward from the inner rect by this amount.
|
|
38
|
+
* @param {string} [options.collisionType='BOUNCE'] - Collision type applied to
|
|
39
|
+
* all walls (a `CollisionTypes` constant).
|
|
40
|
+
* @param {Array<string>} [options.actions=[]] - Collision actions applied to all
|
|
41
|
+
* walls (array of `CollisionActions` constants, e.g. `[BOUNCE, ABSORB]`).
|
|
42
|
+
* @param {Array<number>} [options.color=[120,120,120]] - Wall color `[r,g,b]`.
|
|
43
|
+
* @param {Object} [options.slit] - Optional slit configuration.
|
|
44
|
+
* @param {string} options.slit.wall - Which wall carries the slit:
|
|
45
|
+
* `'right'` | `'left'` | `'top'` | `'bottom'`.
|
|
46
|
+
* @param {number} options.slit.height - Slit opening height (pixels).
|
|
47
|
+
*
|
|
48
|
+
* @deprecated V1 sugar, kept for byte-identical scenes (blackbody's OvenBox). V2 direction: four Wall{ collisionBehavior:'block' | 'bounce' } slabs (needs an opt-in fixed-axis flip to reproduce this wall program).
|
|
49
|
+
*/
|
|
50
|
+
function Box(universe, options) {
|
|
51
|
+
options = options || {};
|
|
52
|
+
|
|
53
|
+
this.universe = universe;
|
|
54
|
+
this._schemaKind = 'Box';
|
|
55
|
+
|
|
56
|
+
var left = options.left;
|
|
57
|
+
var top = options.top;
|
|
58
|
+
var w = options.width;
|
|
59
|
+
var h = options.height;
|
|
60
|
+
var wt = options.wallThickness || 10;
|
|
61
|
+
var ct = options.collisionType || CollisionTypes.BOUNCE;
|
|
62
|
+
var actions = options.actions || [];
|
|
63
|
+
var color = options.color || [120, 120, 120];
|
|
64
|
+
var slit = options.slit || null;
|
|
65
|
+
|
|
66
|
+
// Outer rect (walls extend outward from inner box)
|
|
67
|
+
var outerLeft = left - wt;
|
|
68
|
+
var outerTop = top - wt;
|
|
69
|
+
var outerW = w + 2 * wt;
|
|
70
|
+
var outerH = h + 2 * wt;
|
|
71
|
+
|
|
72
|
+
this.walls = [];
|
|
73
|
+
this.innerRect = { left: left, top: top, w: w, h: h };
|
|
74
|
+
this.outerRect = { left: outerLeft, top: outerTop, w: outerW, h: outerH };
|
|
75
|
+
this.wallThickness = wt;
|
|
76
|
+
|
|
77
|
+
// Top wall
|
|
78
|
+
this.walls.push(new LargeObject(
|
|
79
|
+
universe,
|
|
80
|
+
[outerLeft, outerTop, outerW, wt],
|
|
81
|
+
ct, actions, 'TOP', 'bottom', null, 1, color
|
|
82
|
+
));
|
|
83
|
+
|
|
84
|
+
// Bottom wall
|
|
85
|
+
this.walls.push(new LargeObject(
|
|
86
|
+
universe,
|
|
87
|
+
[outerLeft, outerTop + outerH - wt, outerW, wt],
|
|
88
|
+
ct, actions, 'BOTTOM', 'top', null, 1, color
|
|
89
|
+
));
|
|
90
|
+
|
|
91
|
+
// Left wall
|
|
92
|
+
if (slit && slit.wall === 'left') {
|
|
93
|
+
this._createSlittedWall(universe, 'left', outerLeft, outerTop, outerH, wt, slit.height, ct, actions, color);
|
|
94
|
+
} else {
|
|
95
|
+
this.walls.push(new LargeObject(
|
|
96
|
+
universe,
|
|
97
|
+
[outerLeft, outerTop, wt, outerH],
|
|
98
|
+
ct, actions, 'LEFT', 'right', null, 1, color
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Right wall
|
|
103
|
+
if (slit && slit.wall === 'right') {
|
|
104
|
+
this._createSlittedWall(universe, 'right', outerLeft + outerW - wt, outerTop, outerH, wt, slit.height, ct, actions, color);
|
|
105
|
+
} else {
|
|
106
|
+
this.walls.push(new LargeObject(
|
|
107
|
+
universe,
|
|
108
|
+
[outerLeft + outerW - wt, outerTop, wt, outerH],
|
|
109
|
+
ct, actions, 'RIGHT', 'left', null, 1, color
|
|
110
|
+
));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// THE ONE UP-LINK on every wall LO this Box composed (a drawn wall resolves to the Box).
|
|
114
|
+
for (var _wi = 0; _wi < this.walls.length; _wi++) {
|
|
115
|
+
this.walls[_wi]._owner = this; // THE ONE UP-LINK: the Box composed each wall LO
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create a wall with a slit (gap) in the middle.
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
Box.prototype._createSlittedWall = function (universe, side, wallX, outerTop, outerH, wt, slitHeight, ct, actions, color) {
|
|
124
|
+
var topSegH = (outerH - slitHeight) / 2;
|
|
125
|
+
var reposition = (side === 'left') ? 'right' : 'left';
|
|
126
|
+
var labelSuffix = side.toUpperCase();
|
|
127
|
+
|
|
128
|
+
// Top segment
|
|
129
|
+
this.walls.push(new LargeObject(
|
|
130
|
+
universe,
|
|
131
|
+
[wallX, outerTop, wt, topSegH],
|
|
132
|
+
ct, actions, labelSuffix + '_T', reposition, null, 1, color
|
|
133
|
+
));
|
|
134
|
+
|
|
135
|
+
// Bottom segment
|
|
136
|
+
this.walls.push(new LargeObject(
|
|
137
|
+
universe,
|
|
138
|
+
[wallX, outerTop + outerH - topSegH - 1, wt, topSegH],
|
|
139
|
+
ct, actions, labelSuffix + '_B', reposition, null, 1, color
|
|
140
|
+
));
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set collision type on all walls.
|
|
145
|
+
* @param {string} ct - CollisionTypes value
|
|
146
|
+
*/
|
|
147
|
+
Box.prototype.setCollisionType = function (ct) {
|
|
148
|
+
for (var i = 0; i < this.walls.length; i++) {
|
|
149
|
+
this.walls[i].setCollisionType(ct);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Enable/disable all walls.
|
|
155
|
+
* @param {boolean} active
|
|
156
|
+
*/
|
|
157
|
+
Box.prototype.setActive = function (active) {
|
|
158
|
+
for (var i = 0; i < this.walls.length; i++) {
|
|
159
|
+
this.walls[i].setActive(active);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Full teardown of what this Box composed: every wall LO leaves the world (dispose IS the
|
|
165
|
+
* removal). Idempotent (LargeObject.dispose guards itself).
|
|
166
|
+
*/
|
|
167
|
+
Box.prototype.dispose = function () {
|
|
168
|
+
for (var i = 0; i < this.walls.length; i++) { this.walls[i].dispose(); }
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ── The kind (objects/kinds.js): descriptor, construction defaults, builder ──────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Box — Rectangular enclosure made of 4 LargeObject walls (optionally with a slit).
|
|
175
|
+
* Constructor: Box(universe, options)
|
|
176
|
+
* options: { left, top, width, height, wallThickness, collisionType, actions, color, slit }
|
|
177
|
+
* Group wrapper: all wall sub-LargeObjects are stamped with the ONE up-link `_owner` = <this>.
|
|
178
|
+
*/
|
|
179
|
+
var BoxDescriptor = {
|
|
180
|
+
description: 'A rectangular enclosure built from 4 LargeObject walls ' +
|
|
181
|
+
'(top, bottom, left, right). Any wall can optionally have a slit opening. ' +
|
|
182
|
+
'Used to confine particles, build cavities, or create directed channels. ' +
|
|
183
|
+
'Inner dimensions define the open cavity; walls extend outward by wallThickness.',
|
|
184
|
+
fields: [
|
|
185
|
+
{
|
|
186
|
+
name: 'left',
|
|
187
|
+
type: 'number',
|
|
188
|
+
constraint: { integer: true },
|
|
189
|
+
engineVisible: true,
|
|
190
|
+
description: 'Inner box left X position (physics pixels). The outer wall begins at left - wallThickness.'
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'top',
|
|
194
|
+
type: 'number',
|
|
195
|
+
constraint: { integer: true },
|
|
196
|
+
engineVisible: true,
|
|
197
|
+
description: 'Inner box top Y position (physics pixels). The outer wall begins at top - wallThickness.'
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'width',
|
|
201
|
+
type: 'number',
|
|
202
|
+
constraint: { min: 1, integer: true },
|
|
203
|
+
engineVisible: true,
|
|
204
|
+
description: 'Inner box width (physics pixels). Outer width = width + 2 * wallThickness.'
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'height',
|
|
208
|
+
type: 'number',
|
|
209
|
+
constraint: { min: 1, integer: true },
|
|
210
|
+
engineVisible: true,
|
|
211
|
+
description: 'Inner box height (physics pixels). Outer height = height + 2 * wallThickness.'
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'wallThickness',
|
|
215
|
+
type: 'number',
|
|
216
|
+
constraint: { min: 1, integer: true },
|
|
217
|
+
engineVisible: true,
|
|
218
|
+
unit: 'px',
|
|
219
|
+
description: 'Thickness of each wall in physics pixels (default 10). Walls extend outward from the inner box.'
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'collisionType',
|
|
223
|
+
type: 'enum',
|
|
224
|
+
constraint: { values: CollisionTypes },
|
|
225
|
+
engineVisible: true,
|
|
226
|
+
description: 'Collision type applied to all four walls (e.g. BOUNCE confines particles inside).'
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'actions',
|
|
230
|
+
type: 'enum-set',
|
|
231
|
+
constraint: { values: CollisionActions },
|
|
232
|
+
engineVisible: true,
|
|
233
|
+
description: 'Actions applied on collision for all four walls (e.g. [BOUNCE, ABSORB]).'
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'color',
|
|
237
|
+
type: 'rgb',
|
|
238
|
+
engineVisible: false,
|
|
239
|
+
live: true,
|
|
240
|
+
description: 'Render color [r, g, b] for all four walls (render-only). A live edit fans ' +
|
|
241
|
+
'out to every wall the group owns (the `_owner` fan-out in updateObjectProperty).'
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
registerKind(Box, {
|
|
247
|
+
from: import.meta.url,
|
|
248
|
+
descriptor: BoxDescriptor,
|
|
249
|
+
defaults: {
|
|
250
|
+
left: 60, top: 60,
|
|
251
|
+
width: 80, height: 80,
|
|
252
|
+
wallThickness: 10,
|
|
253
|
+
collisionType: 'BOUNCE',
|
|
254
|
+
actions: [],
|
|
255
|
+
color: [120, 120, 120]
|
|
256
|
+
},
|
|
257
|
+
build: function (props, ctx, Class) {
|
|
258
|
+
var BoxCls = Class; // the class that registered (a composed fork's own class)
|
|
259
|
+
var box = new BoxCls(ctx.runner.universe, Object.assign({
|
|
260
|
+
left: props.left != null ? props.left : Math.round(ctx.x - 40),
|
|
261
|
+
top: props.top != null ? props.top : Math.round(ctx.y - 40),
|
|
262
|
+
width: 80, height: 80,
|
|
263
|
+
wallThickness: 10, collisionType: 'BOUNCE', actions: [], color: [120, 120, 120]
|
|
264
|
+
}, props));
|
|
265
|
+
box._schemaKind = Class.name;
|
|
266
|
+
return box;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ── Export ───────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
export { Box };
|
|
273
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bullet — Engine-agnostic moving projectile sugar.
|
|
5
|
+
*
|
|
6
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* A small `LargeObject` that advances horizontally each tick via
|
|
9
|
+
* `universe.updateObjectProperty(handle, 'LargeObject', 'rect', [...])`.
|
|
10
|
+
* The tick function is pushed onto `universe.experimentTickFunctions` so the
|
|
11
|
+
* Bullet self-drives: no external per-tick call needed.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} universe - Engine universe instance (JS or WASM compat shim),
|
|
14
|
+
* already init'd (init() is called by the enclosing setup factory, not here).
|
|
15
|
+
* @param {Object} [opts] - Configuration overrides.
|
|
16
|
+
* @param {number} [opts.x] - Left edge of the bullet rect (pixels). Default: 0.
|
|
17
|
+
* @param {number} [opts.y] - Top edge of the bullet rect (pixels). Default: canvas centre.
|
|
18
|
+
* @param {number} [opts.w=8] - Bullet rect width (pixels). Units: pixels.
|
|
19
|
+
* @param {number} [opts.h=8] - Bullet rect height (pixels). Units: pixels.
|
|
20
|
+
* @param {number} [opts.speed=2] - Horizontal advance per tick (pixels/tick). Positive = rightward.
|
|
21
|
+
* Keeps within [−1,+1] per-axis speed limit PER MECHANISM — the bullet itself is not a
|
|
22
|
+
* quantum, so this limit does not apply here. OBSERVED: speed 2 makes the wake
|
|
23
|
+
* dense enough to trigger aggregation into bigger frames. speed 1 makes a wispy trail.
|
|
24
|
+
* @param {string} [opts.collisionType='BOUNCE'] - CollisionTypes value for quanta that
|
|
25
|
+
* intersect the bullet. BOUNCE = deflect quanta (generates the most billowing wake).
|
|
26
|
+
* NONE = ghost (visual-only, no physics). WALL = stops quanta dead (hard shadow).
|
|
27
|
+
* @param {Array} [opts.color=[40,40,40]] - RGB color for the bullet LO rect (renderer).
|
|
28
|
+
* @param {string} [opts.name='bullet'] - LargeObject name tag.
|
|
29
|
+
*
|
|
30
|
+
* @property {Object} lo - The underlying LargeObject instance.
|
|
31
|
+
* @property {*} handle - The engine handle for the LargeObject (opaque).
|
|
32
|
+
* @property {number} vx - Current horizontal speed (px/tick).
|
|
33
|
+
*
|
|
34
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* The moving exclude region (BOUNCE) compresses quanta in front and deflects
|
|
37
|
+
* them to the sides, identical to the static `block 2` shadow in the
|
|
38
|
+
* Wave-Interference experiment. **The billowing wake is free physics** — no
|
|
39
|
+
* explicit wake/smoke code.
|
|
40
|
+
*
|
|
41
|
+
* When the enclosing setup factory also enables `universe.withInterQuantumCollisions`
|
|
42
|
+
* and registers extra dimensions (the Blackbody aggregation mechanic), the compressed
|
|
43
|
+
* quanta behind the bullet parent up the voxel-size ladder into bigger frames. The
|
|
44
|
+
* renderer (three-provider.js) sizes each puff by `frame.rect.w`, so the emergent
|
|
45
|
+
* frame-size variation produces the smoke-billow effect: faster bullet → more compression
|
|
46
|
+
* → more collisions → bigger frames → more prominent puffs.
|
|
47
|
+
*
|
|
48
|
+
* The bullet wraps the canvas if `opts.wrap` is true (default: false — the bullet
|
|
49
|
+
* exits at the right edge and the tick function becomes a no-op when out of bounds).
|
|
50
|
+
* Set `opts.loop` to restart from the left when the bullet exits the right edge.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { LargeObject } from '../../parts/LargeObject.js';
|
|
54
|
+
import { CollisionTypes } from '../../parts/CollisionTypes.js';
|
|
55
|
+
import { ObjectSchema } from '../schema/index.js';
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// ── Bullet ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a moving projectile and register its advance tick function.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} universe - Already-init'd universe (JS or WASM).
|
|
64
|
+
* @param {Object} [opts] - See module-level JSDoc above.
|
|
65
|
+
*/
|
|
66
|
+
function Bullet(universe, opts) {
|
|
67
|
+
opts = opts || {};
|
|
68
|
+
|
|
69
|
+
// The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
|
|
70
|
+
var world = ObjectSchema.worldRect(universe.universe_rect, 'Bullet universe.universe_rect');
|
|
71
|
+
var cw = world.w;
|
|
72
|
+
var ch = world.h;
|
|
73
|
+
|
|
74
|
+
var bw = opts.w !== undefined ? opts.w : 8;
|
|
75
|
+
var bh = opts.h !== undefined ? opts.h : 8;
|
|
76
|
+
var bx = opts.x !== undefined ? opts.x : 0;
|
|
77
|
+
var by = opts.y !== undefined ? opts.y : Math.floor(ch / 2) - Math.floor(bh / 2);
|
|
78
|
+
var vx = opts.speed !== undefined ? opts.speed : 2;
|
|
79
|
+
var ct = opts.collisionType !== undefined ? opts.collisionType : CollisionTypes.BOUNCE;
|
|
80
|
+
var col = opts.color !== undefined ? opts.color : [40, 40, 40];
|
|
81
|
+
var name = opts.name !== undefined ? opts.name : 'bullet';
|
|
82
|
+
var loop = opts.loop !== undefined ? !!opts.loop : true;
|
|
83
|
+
// Eject mode for the BOUNCE op-program. 2D default 'closest' (4 faces); a 3D scene
|
|
84
|
+
// (activeAxes.z) passes 'closest3d' so quanta can be pushed out the near/far z faces too.
|
|
85
|
+
var repo = opts.reposition !== undefined ? opts.reposition : 'closest';
|
|
86
|
+
|
|
87
|
+
// Mutable state — advanced by the tick function below.
|
|
88
|
+
// rect is [left, top, w, h] (is_centered=false convention, same as LargeObject).
|
|
89
|
+
var _left = bx;
|
|
90
|
+
var _top = by;
|
|
91
|
+
|
|
92
|
+
this.lo = new LargeObject(
|
|
93
|
+
universe,
|
|
94
|
+
[_left, _top, bw, bh],
|
|
95
|
+
CollisionTypes.NONE, ['BOUNCE'],
|
|
96
|
+
name, repo, null, 1,
|
|
97
|
+
col
|
|
98
|
+
);
|
|
99
|
+
// ── Render shape (renderer-only hints; physics is unaffected) ──────────────
|
|
100
|
+
// shape:'box' → the provider wraps the LO with a 3D BoxGeometry (a solid cube of
|
|
101
|
+
// w×h×depth) instead of a flat plane, so the projectile reads as a real 3D object
|
|
102
|
+
// from every angle. A box has volume, so it does NOT billboard. Any other shape
|
|
103
|
+
// (default: a flat plane) billboards to face the camera (a disc coming AT the viewer).
|
|
104
|
+
this.lo.shape = (opts.shape !== undefined) ? opts.shape : 'box';
|
|
105
|
+
this.lo.depth = (opts.depth !== undefined) ? opts.depth : bw; // cube z-extent
|
|
106
|
+
if (this.lo.shape === 'box') {
|
|
107
|
+
this.lo.billboard = false; // a cube doesn't billboard
|
|
108
|
+
} else {
|
|
109
|
+
this.lo.billboard = (opts.billboard !== undefined) ? !!opts.billboard : true;
|
|
110
|
+
}
|
|
111
|
+
this.handle = this.lo.handle;
|
|
112
|
+
this.vx = vx;
|
|
113
|
+
|
|
114
|
+
var self = this;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Advance the bullet one tick.
|
|
118
|
+
* Registered on universe.experimentTickFunctions so this runs as Phase A
|
|
119
|
+
* injection for the next tick.
|
|
120
|
+
*
|
|
121
|
+
* When the bullet exits the right edge (left > cw) and opts.loop is true,
|
|
122
|
+
* it wraps back to the left edge so the demo runs continuously.
|
|
123
|
+
* When loop is false the rect is left at the exit position — the LO
|
|
124
|
+
* still exists but no longer intersects any quanta.
|
|
125
|
+
*/
|
|
126
|
+
this.tick = function () {
|
|
127
|
+
_left += self.vx;
|
|
128
|
+
if (_left > cw) {
|
|
129
|
+
if (loop) {
|
|
130
|
+
_left = -bw; // re-enter from left
|
|
131
|
+
} else {
|
|
132
|
+
return; // off-screen — stop advancing
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Mutate the JS LargeObject wrapper's rect so the RENDERER moves the bullet —
|
|
136
|
+
// it reads the world census entry's .rect each frame. `universe.updateObjectProperty`
|
|
137
|
+
// does NOT touch the JS wrapper (only FrqtlApp.updateObjectProperty does —
|
|
138
|
+
// see wasm-engine.js:513-516 + FrqtlApp.js:422); the bullet tick-fn only
|
|
139
|
+
// has `universe`, so we mirror that wrapper-mutate here (Rectangle .left/.top setters).
|
|
140
|
+
self.lo.rect.left = _left;
|
|
141
|
+
self.lo.rect.top = _top;
|
|
142
|
+
// Move the engine-side collision region too, so the compression wake follows the bullet.
|
|
143
|
+
universe.updateObjectProperty(
|
|
144
|
+
self.handle, 'LargeObject', 'rect',
|
|
145
|
+
[_left, _top, bw, bh]
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
universe.experimentTickFunctions.push(this.tick);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Export ───────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export { Bullet };
|
|
155
|
+
|