@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,297 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ObjectFactory — registry-driven construction of all placeable object kinds.
|
|
5
|
+
*
|
|
6
|
+
* This is the ONE canonical construction path for every IDE-placeable kind.
|
|
7
|
+
* It was extracted from lib/features/ide/project-design.js `buildObject` in
|
|
8
|
+
* Phase 1 of the ObjectFactory refactor so the construction logic lives in
|
|
9
|
+
* the objects layer (engine-agnostic) rather than in the IDE feature layer.
|
|
10
|
+
*
|
|
11
|
+
* API:
|
|
12
|
+
* ObjectFactory.create(kind, config, ctx)
|
|
13
|
+
* kind — placeable kind string (e.g. 'QuantumEmitter', 'SlitScreen')
|
|
14
|
+
* config — override props (merged over schema defaultProps)
|
|
15
|
+
* ctx — { runner, x, y, fromPalette? }
|
|
16
|
+
* The world size is READ from ctx.runner.canvas_rect — the rect the scene's
|
|
17
|
+
* co-init call wrote (exp.createCanvas(w, h, cfg) / exp.createScene(w, h, d, cfg));
|
|
18
|
+
* ctx carries no size of its own.
|
|
19
|
+
* returns: the DECLARED part (recorded in ctx.runner.declared); THROWS on a kind no
|
|
20
|
+
* module registered in this realm, on any build result that is not a handle,
|
|
21
|
+
* and on a member LO whose `_owner` chain does not end at the part
|
|
22
|
+
*
|
|
23
|
+
* ObjectFactory.list() → ObjectSchema.placeableCatalog()
|
|
24
|
+
* ObjectFactory.kinds() → the registered kinds that carry a build
|
|
25
|
+
*
|
|
26
|
+
* Construction: the kind's registry row (objects/kinds.js — the part module registered its
|
|
27
|
+
* descriptor, defaults and build when it was imported) → the generic checks below → the row's
|
|
28
|
+
* `build(props, ctx)`.
|
|
29
|
+
*
|
|
30
|
+
* Behavior contract:
|
|
31
|
+
* - Every special case from buildObject is preserved verbatim:
|
|
32
|
+
* • QuantumEmitter / PointSource / SphereEmitter: the emitter + a visual
|
|
33
|
+
* LargeObject marker it OWNS (attachMarker); the emitter is returned.
|
|
34
|
+
* • A builder stamps nothing: each part stamps `_owner` on what it composed,
|
|
35
|
+
* and create() declares the returned part with the LOs the build added.
|
|
36
|
+
* • PointSource opts dict re-pass (_wavelength, _optColor, _emergentPhysics).
|
|
37
|
+
* • layer → observedDimensions=[layer] (null = all dims).
|
|
38
|
+
* • SlitLightSource: THROWS naming the pairing (needs a paired SlitScreen).
|
|
39
|
+
* • Unknown kind: THROW naming the registered kinds + the V2 replacement.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { ObjectSchema } from './schema/index.js';
|
|
43
|
+
import { kindOf, registeredKinds, unregisteredKindMessage } from './kinds.js';
|
|
44
|
+
import { setupSphereEmitter3D } from './setup/setupSphereEmitter3D.js';
|
|
45
|
+
import { SphereEmitter } from '../parts/SphereEmitter.js';
|
|
46
|
+
import { V1_REPLACEMENTS } from './v1-replacements.js';
|
|
47
|
+
|
|
48
|
+
// ── Internal helpers ──────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function _deepCopy(obj) {
|
|
51
|
+
if (obj === null || typeof obj !== 'object') { return obj; }
|
|
52
|
+
if (Array.isArray(obj)) { return obj.map(_deepCopy); }
|
|
53
|
+
var out = {};
|
|
54
|
+
for (var k in obj) {
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(obj, k)) { out[k] = _deepCopy(obj[k]); }
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Merge schema defaultProps with caller overrides and return the merged props.
|
|
62
|
+
* @param {string} kind
|
|
63
|
+
* @param {object|null} config — override props (caller-supplied)
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
// Construction defaults come from ObjectSchema.defaultPropsFor(kind) — the ONE
|
|
67
|
+
// per-kind default bag (the `defaults` the kind registered with, objects/kinds.js). NOT from
|
|
68
|
+
// placeableCatalog(): the palette answers "what can be DROPPED in STUDIO", which
|
|
69
|
+
// is a strictly smaller set (V2 stock only) than "what a recipe can construct".
|
|
70
|
+
//
|
|
71
|
+
function _mergeProps(kind, config) {
|
|
72
|
+
var defaults = _deepCopy(ObjectSchema.defaultPropsFor(kind));
|
|
73
|
+
return Object.assign({}, defaults, config || {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── CONSTRUCTION_ADAPTERS — ObjectFactory's OWN cross-kind config vocabulary ──
|
|
77
|
+
//
|
|
78
|
+
// `create()` already threw on an unknown KIND with the registered list and the
|
|
79
|
+
// V1→V2 replacement. It never looked at the config's PROP KEYS, so
|
|
80
|
+
// `create('Wall', { collisonBehavior: 'bounce' })` built a wall that ignored the
|
|
81
|
+
// typo and silently used its own default — the same silence the setup-factory gate
|
|
82
|
+
// removes one layer up.
|
|
83
|
+
//
|
|
84
|
+
// The schema descriptor alone is NOT the legal set. These keys are read by the
|
|
85
|
+
// builders (or forwarded verbatim into an object constructor) while being declared
|
|
86
|
+
// by no kind's field list, so validating on the descriptor alone would reject
|
|
87
|
+
// working callers. They are ObjectFactory's own construction API — it owns the call
|
|
88
|
+
// shape — so they are declared HERE, and `objectfactory-prop-keys.test.mjs` asserts
|
|
89
|
+
// each one is genuinely reachable rather than a list someone widened to silence a
|
|
90
|
+
// failure.
|
|
91
|
+
var CONSTRUCTION_ADAPTERS = [
|
|
92
|
+
// Geometry/identity every builder resolves a rect and a name from. `x`/`y` are
|
|
93
|
+
// read by RecipeContext.addObject to build ctx before create() runs.
|
|
94
|
+
'x', 'y', 'w', 'h', 'left', 'top', 'rect', 'layer', 'name',
|
|
95
|
+
// `preset` is expanded by the OBJECT constructor (Wall.expandWallPreset), not by
|
|
96
|
+
// a builder — it reaches the constructor through the forwarded props bag.
|
|
97
|
+
'preset',
|
|
98
|
+
// Codegen round-trip adapters: a PointSource's construction-only opts, which codegen
|
|
99
|
+
// writes back onto its config (lib/features/ide/codegen.js).
|
|
100
|
+
'_wavelength', '_optColor', '_emergentPhysics'
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// ── FORWARDED — kinds whose config bag is a PART's option bag, not just a descriptor ──
|
|
104
|
+
//
|
|
105
|
+
// A facade kind hands its config to a setup factory, so its legal set includes that
|
|
106
|
+
// factory's vocabulary. Measured: `emissionBoxSide` is a real SphereEmitter option
|
|
107
|
+
// that the descriptor does not declare, and gating on the descriptor alone would
|
|
108
|
+
// reject the walkthrough's own recipes.
|
|
109
|
+
//
|
|
110
|
+
// CONSUMED, never retyped: `setupSphereEmitter3D.OPTIONS` and
|
|
111
|
+
// `SphereEmitter.FACADE_OPTIONS` are the owners' exported contracts.
|
|
112
|
+
function _forwardedVocabulary(kind) {
|
|
113
|
+
if (kind !== 'SphereEmitter') { return []; }
|
|
114
|
+
var out = [];
|
|
115
|
+
var f = setupSphereEmitter3D;
|
|
116
|
+
var o = SphereEmitter;
|
|
117
|
+
if (f.OPTIONS) { out = out.concat(f.OPTIONS); }
|
|
118
|
+
// Retired names pass THROUGH this gate so the owner's specific migration message
|
|
119
|
+
// wins over a generic "unknown prop key" (the a1f1b324 control-D ordering, again).
|
|
120
|
+
if (f.RETIRED_OPTIONS) { out = out.concat(f.RETIRED_OPTIONS); }
|
|
121
|
+
if (o.FACADE_OPTIONS) { out = out.concat(o.FACADE_OPTIONS); }
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Throw on a config key that no kind, default, adapter or forwarded vocabulary
|
|
127
|
+
* declares. Every list is DERIVED (from the schema `create()` already resolved, or
|
|
128
|
+
* from an owner's export) — nothing here re-enumerates another module's fields.
|
|
129
|
+
*/
|
|
130
|
+
function _assertPropKeys(kind, config) {
|
|
131
|
+
if (!config) { return; }
|
|
132
|
+
var descriptor = ObjectSchema.get(kind);
|
|
133
|
+
if (!descriptor || !descriptor.fields) { return; }
|
|
134
|
+
|
|
135
|
+
var legal = {};
|
|
136
|
+
descriptor.fields.forEach(function (f) { legal[f.name] = true; });
|
|
137
|
+
Object.keys(ObjectSchema.defaultPropsFor(kind)).forEach(function (k) { legal[k] = true; });
|
|
138
|
+
CONSTRUCTION_ADAPTERS.forEach(function (k) { legal[k] = true; });
|
|
139
|
+
_forwardedVocabulary(kind).forEach(function (k) { legal[k] = true; });
|
|
140
|
+
|
|
141
|
+
var unknown = Object.keys(config).filter(function (k) { return !legal[k]; });
|
|
142
|
+
if (!unknown.length) { return; }
|
|
143
|
+
|
|
144
|
+
var declaredFields = descriptor.fields.map(function (f) { return f.name; }).sort();
|
|
145
|
+
throw new Error(
|
|
146
|
+
'[ObjectFactory] create("' + kind + '"): unknown prop key' +
|
|
147
|
+
(unknown.length > 1 ? 's' : '') + ' ' + unknown.map(function (k) {
|
|
148
|
+
return '"' + k + '"';
|
|
149
|
+
}).join(', ') + '. The builder reads only declared props, so this would have ' +
|
|
150
|
+
'been merged in, read by nobody, and the object built with ITS OWN default — ' +
|
|
151
|
+
'a different scene than the one you wrote, with no signal. "' + kind +
|
|
152
|
+
'" declares: ' + declaredFields.join(', ') +
|
|
153
|
+
'. Construction adapters: ' + CONSTRUCTION_ADAPTERS.slice().sort().join(', ') + '.'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A `writeOnly` descriptor field is a live reposition ADAPTER (a SphereEmitter's or a Tunnel's
|
|
159
|
+
* `rect`): it moves a built part, and its read getter derives from the part's real position
|
|
160
|
+
* fields. It is never a construction key — the part is built from those real fields — so a
|
|
161
|
+
* config carrying one THROWS here rather than being dropped silently by the builder.
|
|
162
|
+
*/
|
|
163
|
+
function _refuseWriteOnlyKeys(kind, config) {
|
|
164
|
+
if (!config) { return; }
|
|
165
|
+
var descriptor = ObjectSchema.get(kind);
|
|
166
|
+
if (!descriptor || !descriptor.fields) { return; }
|
|
167
|
+
var wo = descriptor.fields.filter(function (fd) { return fd.writeOnly === true && config[fd.name] !== undefined; });
|
|
168
|
+
if (!wo.length) { return; }
|
|
169
|
+
var real = descriptor.fields.filter(function (fd) { return !fd.writeOnly; }).map(function (fd) { return fd.name; });
|
|
170
|
+
throw new Error('[ObjectFactory] create("' + kind + '"): "' + wo[0].name + '" is a write-only reposition ' +
|
|
171
|
+
'adapter (it moves a built ' + kind + '), not a construction key. Declare the position with the ' +
|
|
172
|
+
'fields the part is built from: ' + real.join(', ') + '. (FAIL-LOUD.)');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
// How many objects of `kind` this runner has declared — the ordinal a numbered default name
|
|
177
|
+
// ('emitter N', 'source N') carries.
|
|
178
|
+
function declaredCount(runner, kind) {
|
|
179
|
+
return runner.declared.filter(function (d) { return d._schemaKind === kind; }).length;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── ObjectFactory ─────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
var ObjectFactory = {
|
|
185
|
+
/**
|
|
186
|
+
* Construct a placeable object of the given kind and DECLARE it: the part the builder
|
|
187
|
+
* returns (never one of its members) is recorded in `ctx.runner.declared` with the census LOs
|
|
188
|
+
* the build added as its members and the host tick-functions the build pushed
|
|
189
|
+
* (`FrqtlApp.declare` — this is its ONE call site). What a
|
|
190
|
+
* declaring call (`exp.addObject`, a palette drop, a codegen replay) produced is what STUDIO
|
|
191
|
+
* holds; a part's composition is never declared.
|
|
192
|
+
*
|
|
193
|
+
* FAIL-LOUD: an unknown kind THROWS naming the registered kinds and, when
|
|
194
|
+
* the kind is a removed V1 part, its V2 replacement composition. A recipe
|
|
195
|
+
* whose builder no longer exists must never half-run into a scene with
|
|
196
|
+
* missing parts.
|
|
197
|
+
*
|
|
198
|
+
* @param {string|Function} kind — the kind name, or its registered class
|
|
199
|
+
* @param {object|null} config — override props (merged over schema defaultProps)
|
|
200
|
+
* @param {object} ctx — { runner, x, y, fromPalette? } (the size is runner.canvas_rect)
|
|
201
|
+
* @returns {object} — the declared part (a Tunnel, not its plate; a SphereEmitter, not its marker)
|
|
202
|
+
* @throws {Error} on a kind no module registered here, a kind registered without a build,
|
|
203
|
+
* a ctx without a runner, or a build result that is not a handle
|
|
204
|
+
*/
|
|
205
|
+
create: function (kind, config, ctx) {
|
|
206
|
+
if (!ctx || !ctx.runner) {
|
|
207
|
+
throw new Error('[ObjectFactory] create("' + kind + '"): ctx.runner is required — ' +
|
|
208
|
+
'pass { runner, x, y }.');
|
|
209
|
+
}
|
|
210
|
+
if (typeof kind === 'function') {
|
|
211
|
+
// A kind is named by its class: the class itself is the import-form spelling.
|
|
212
|
+
if (!kindOf(kind)) {
|
|
213
|
+
throw new Error('[ObjectFactory] create: the class ' + (kind.name || '(anonymous)') + ' is not a ' +
|
|
214
|
+
'registered kind in this realm — ' + unregisteredKindMessage(kind.name || '?') + '. (FAIL-LOUD.)');
|
|
215
|
+
}
|
|
216
|
+
kind = kind.name;
|
|
217
|
+
}
|
|
218
|
+
var row = kindOf(kind);
|
|
219
|
+
if (!row) {
|
|
220
|
+
var msg = '[ObjectFactory] create: unknown kind "' + kind + '". Registered kinds: ' +
|
|
221
|
+
ObjectFactory.kinds().slice().sort().join(', ') + '. The ' + unregisteredKindMessage(kind) + '.';
|
|
222
|
+
if (Object.prototype.hasOwnProperty.call(V1_REPLACEMENTS, kind)) {
|
|
223
|
+
msg += ' "' + kind + '" is a retired V1 part, not a published part — compose ' + V1_REPLACEMENTS[kind] + ' instead ' +
|
|
224
|
+
'(docs/internal/engine/V2_PARTS_CLASSIFICATION.md).';
|
|
225
|
+
}
|
|
226
|
+
throw new Error(msg + ' (FAIL-LOUD.)');
|
|
227
|
+
}
|
|
228
|
+
if (typeof row.build !== 'function') {
|
|
229
|
+
throw new Error('[ObjectFactory] create: kind "' + kind + '" registers a descriptor only — it is not ' +
|
|
230
|
+
'constructed by ObjectFactory; construct it with new ' + kind + '(…) (' + row.from + '). (FAIL-LOUD.)');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ORDER IS LOAD-BEARING: the unknown-KIND throw runs first (it names the V1→V2
|
|
234
|
+
// replacement), then the prop-key check, then the merge. Validating props for a
|
|
235
|
+
// kind that does not exist would replace a useful migration message with a
|
|
236
|
+
// confusing field list.
|
|
237
|
+
_assertPropKeys(kind, config);
|
|
238
|
+
_refuseWriteOnlyKeys(kind, config);
|
|
239
|
+
|
|
240
|
+
var props = _mergeProps(kind, config);
|
|
241
|
+
|
|
242
|
+
var runner = ctx.runner;
|
|
243
|
+
var before = new Set(runner.largeObjects);
|
|
244
|
+
var tfs = (runner.universe && Array.isArray(runner.universe.experimentTickFunctions))
|
|
245
|
+
? runner.universe.experimentTickFunctions : null;
|
|
246
|
+
var ticksBefore = new Set(tfs || []);
|
|
247
|
+
var handle = row.build(props, ctx, row.Class); // the registry builds the class that registered
|
|
248
|
+
if (handle === null || typeof handle !== 'object') {
|
|
249
|
+
throw new Error('[ObjectFactory] create("' + kind + '"): the builder returned ' +
|
|
250
|
+
(handle === null ? 'null' : typeof handle) + ', not a handle. Every kind returns ' +
|
|
251
|
+
'the object it built or throws naming what is missing — a non-handle here is a ' +
|
|
252
|
+
'part silently absent from the scene. (FAIL-LOUD.)');
|
|
253
|
+
}
|
|
254
|
+
if (typeof runner.declare !== 'function') {
|
|
255
|
+
throw new Error('[ObjectFactory] create("' + kind + '"): ctx.runner has no declare() — the ' +
|
|
256
|
+
'runner (FrqtlApp) owns the declared-object registry every created part is recorded in. ' +
|
|
257
|
+
'(FAIL-LOUD.)');
|
|
258
|
+
}
|
|
259
|
+
// THE ONE REGISTRATION SITE: what a declaring call produced is the declared object; its
|
|
260
|
+
// members are exactly the census LOs the build added (each must resolve up `_owner` to it).
|
|
261
|
+
var members = runner.largeObjects.filter(function (lo) { return !before.has(lo); });
|
|
262
|
+
if (!members.length) {
|
|
263
|
+
throw new Error('[ObjectFactory] create("' + kind + '"): the build put nothing in the world — ' +
|
|
264
|
+
'every part draws at least one LO, so a part with none is silently absent from the ' +
|
|
265
|
+
'scene. (FAIL-LOUD.)');
|
|
266
|
+
}
|
|
267
|
+
// ... and the host tick-functions it pushed are the declaration's too (an emitter's emit
|
|
268
|
+
// fn, an absorb radiator): behaviour the declaration owns, so codegen may write it down.
|
|
269
|
+
var tfsAfter = (runner.universe && Array.isArray(runner.universe.experimentTickFunctions))
|
|
270
|
+
? runner.universe.experimentTickFunctions : [];
|
|
271
|
+
var ticks = tfsAfter.filter(function (fn) { return !ticksBefore.has(fn); });
|
|
272
|
+
runner.declare(handle, members, ticks);
|
|
273
|
+
return handle;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Returns the schema-driven palette catalog.
|
|
278
|
+
* Delegates to ObjectSchema.placeableCatalog().
|
|
279
|
+
* @returns {Array}
|
|
280
|
+
*/
|
|
281
|
+
list: function () {
|
|
282
|
+
return ObjectSchema.placeableCatalog();
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The registered kinds that carry a build (constructible through create()).
|
|
287
|
+
* @returns {string[]}
|
|
288
|
+
*/
|
|
289
|
+
kinds: function () {
|
|
290
|
+
return registeredKinds().filter(function (k) { return typeof kindOf(k).build === 'function'; });
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// ── Export ────────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
export { ObjectFactory, declaredCount };
|
|
297
|
+
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecipeContext — the recipe facade (`exp`) a scene's buildExperiment(exp) is evaluated against.
|
|
5
|
+
*
|
|
6
|
+
* A scene's FIRST statement co-initializes its display and its universe — one call, one rect,
|
|
7
|
+
* named by the renderer it initializes (the Pixi `new Application({width, height})` / p5
|
|
8
|
+
* `createCanvas(w, h)` line of a FRQTL sketch):
|
|
9
|
+
*
|
|
10
|
+
* exp.createCanvas(w, h, worldCfg) p5: a 2D canvas AND a 2D universe of exactly w×h
|
|
11
|
+
* exp.createScene(w, h, d, worldCfg) three: a 3D scene AND a 3D universe w×h×d
|
|
12
|
+
*
|
|
13
|
+
* That call is the ONLY place a world size is written: the facade is constructed SIZELESS
|
|
14
|
+
* (`{canvasParentId, seed, onTick, …}`) and builds its runner INSIDE the call
|
|
15
|
+
* (`new FrqtlApp({canvasW: w, canvasH: h, …})` → `runner.createCanvas()` →
|
|
16
|
+
* `universe.init(...FrqtlWorldInit.initArgs(cfg))`). `worldCfg` is the world
|
|
17
|
+
* vocabulary minus what the call owns (`FrqtlWorldInit.INIT_CFG`; a size, `activeAxes` or
|
|
18
|
+
* `depth` in it THROWS naming this call). 2D ⇒ the engine's {x,y} default; 3D ⇒ {x,y,z} and the
|
|
19
|
+
* explicit depth d.
|
|
20
|
+
*
|
|
21
|
+
* exp.world the ONE readable answer: frozen {renderer, w, h, d, cfg}
|
|
22
|
+
* exp.addObject(kind, config) ObjectFactory.create(kind, config, ctx) sugar
|
|
23
|
+
* exp.addTickFunction(fn) universe.experimentTickFunctions.push
|
|
24
|
+
* exp.render(bag) runner.setRenderOptions(bag)
|
|
25
|
+
* exp.build(buildFn) run the recipe WITHOUT starting the loop (a sketch held as
|
|
26
|
+
* TEXT is a project, linked first: lib/shim/project/link.js linkProject)
|
|
27
|
+
* exp.run(buildFn) build (if needed) + start the loop
|
|
28
|
+
* exp.runner / .universe the runner the co-init call built
|
|
29
|
+
* exp.is3D world.renderer === 'three'
|
|
30
|
+
*
|
|
31
|
+
* FAIL-LOUD: every member that needs the world THROWS before the call; a second call in one
|
|
32
|
+
* recipe run THROWS; a call naming a renderer the host did not boot THROWS; a recipe that returns
|
|
33
|
+
* without a world THROWS from build(); `exp.init(cfg)` is DELETED and throws naming the two
|
|
34
|
+
* methods. None degrades to a default size.
|
|
35
|
+
*
|
|
36
|
+
* The page's facade: build()/run() make THIS facade the page's experiment (`pageExperiment()`,
|
|
37
|
+
* read by a host frame through the page door as FrqtlPage.experiment); the runner it built is the
|
|
38
|
+
* page's runner once its loop starts (FrqtlApp `pageRunner()`).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { FrqtlApp } from './FrqtlApp.js';
|
|
42
|
+
import { ObjectFactory } from './ObjectFactory.js';
|
|
43
|
+
import { FrqtlWorldInit } from './setup/_worldInit.js';
|
|
44
|
+
import * as FrqtlDisplay from '../compat/renderer/display-slot.js';
|
|
45
|
+
import { refuseRetiredDoors } from '../compat/engine.js';
|
|
46
|
+
|
|
47
|
+
/** The world-init contract (objects/setup/_worldInit.js). */
|
|
48
|
+
function _worldInit() {
|
|
49
|
+
return FrqtlWorldInit;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The display slot's booted renderer name (FrqtlDisplay — the ONE display owner). */
|
|
53
|
+
function _bootedRenderer() {
|
|
54
|
+
var n = FrqtlDisplay.name();
|
|
55
|
+
if (!n) {
|
|
56
|
+
throw new Error('[RecipeContext] no display is booted (FrqtlDisplay.name() is empty): the host ' +
|
|
57
|
+
'boots the renderer the scene\'s registry row names (renderState.renderer) before the ' +
|
|
58
|
+
'scene runs. (FAIL-LOUD.)');
|
|
59
|
+
}
|
|
60
|
+
return n;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var COINIT = 'exp.createCanvas(w, h, cfg) (p5, 2D) or exp.createScene(w, h, d, cfg) (three, 3D)';
|
|
64
|
+
var NO_WORLD = '[RecipeContext] the scene has not created its world — its first statement is ' + COINIT + '.';
|
|
65
|
+
|
|
66
|
+
// The ctor options a SIZELESS facade forwards to the runner it builds (everything but a size).
|
|
67
|
+
var RUNNER_OPTS = ['canvasParentId', 'seed', 'tickHz', 'drawHz', 'stepsPerFrame', 'onTick', 'onReady', 'engine'];
|
|
68
|
+
var FACADE_OPTS = RUNNER_OPTS.concat(['deferCanvas']);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Object} [opts] — `{canvasParentId, seed, onTick, onReady, tickHz, drawHz, stepsPerFrame,
|
|
72
|
+
* engine, deferCanvas}`. NO size and NO 3D flag: the scene's co-init call states both.
|
|
73
|
+
* `deferCanvas:true` leaves `runner.createCanvas()` to a host mount seam (the notebook realm).
|
|
74
|
+
*/
|
|
75
|
+
function RecipeContext(opts) {
|
|
76
|
+
opts = opts || {};
|
|
77
|
+
Object.keys(opts).forEach(function (k) {
|
|
78
|
+
if (FACADE_OPTS.indexOf(k) === -1) {
|
|
79
|
+
throw new Error('[RecipeContext] unknown option "' + k + '" — the facade is SIZELESS: it takes ' +
|
|
80
|
+
FACADE_OPTS.join(', ') + '. A world size (canvasW/canvasH) or 3D-ness is the scene\'s, ' +
|
|
81
|
+
'stated by ' + COINIT + '. (FAIL-LOUD.)');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
this._opts = opts;
|
|
85
|
+
this._runner = null; // built by the co-init call
|
|
86
|
+
this._world = null; // set by the co-init call; reset by build()
|
|
87
|
+
this._built = false;
|
|
88
|
+
this._buildFn = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _requireWorld(self) {
|
|
92
|
+
if (!self._world) { throw new Error(NO_WORLD); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Object.defineProperty(RecipeContext.prototype, 'world', { get: function () { return this._world; } });
|
|
96
|
+
Object.defineProperty(RecipeContext.prototype, 'runner', {
|
|
97
|
+
get: function () { _requireWorld(this); return this._runner; }
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(RecipeContext.prototype, 'universe', {
|
|
100
|
+
get: function () { _requireWorld(this); return this._runner.universe; }
|
|
101
|
+
});
|
|
102
|
+
/** READ-ONLY: did THIS scene make a 3D world? The method it called answers (three ⇔ 3D). */
|
|
103
|
+
Object.defineProperty(RecipeContext.prototype, 'is3D', {
|
|
104
|
+
get: function () { _requireWorld(this); return this._world.renderer === 'three'; }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── the co-init call ─────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function _coinit(self, method, renderer, w, h, d, cfg) {
|
|
110
|
+
var where = 'RecipeContext.' + method;
|
|
111
|
+
if (self._world) {
|
|
112
|
+
throw new Error('[' + where + '] one world per scene: this recipe already created ' +
|
|
113
|
+
JSON.stringify({ renderer: self._world.renderer, w: self._world.w, h: self._world.h, d: self._world.d }) +
|
|
114
|
+
'. (FAIL-LOUD.)');
|
|
115
|
+
}
|
|
116
|
+
[['w', w], ['h', h]].concat(renderer === 'three' ? [['d', d]] : []).forEach(function (p) {
|
|
117
|
+
if (typeof p[1] !== 'number' || !(p[1] > 0)) {
|
|
118
|
+
throw new Error('[' + where + '] ' + p[0] + ' must be a positive number (got ' + JSON.stringify(p[1]) +
|
|
119
|
+
') — every scene states its whole world' + (p[0] === 'd' ? '; the engine\'s cube default is not relied on' : '') +
|
|
120
|
+
'. (FAIL-LOUD.)');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
cfg = cfg || {};
|
|
124
|
+
var W = _worldInit();
|
|
125
|
+
W.assertInitCfg(where + '(…, cfg)', cfg, null);
|
|
126
|
+
var booted = _bootedRenderer();
|
|
127
|
+
if (booted !== renderer) {
|
|
128
|
+
var err = new Error('[' + where + '] the scene calls ' + method + ' (' + renderer + ') but the host ' +
|
|
129
|
+
'booted "' + booted + '" — the host\'s display (a registry row\'s renderState.renderer, bootScene\'s ' +
|
|
130
|
+
'opts.renderer) and the scene disagree. (FAIL-LOUD.)');
|
|
131
|
+
err.frqtlRequiredRenderer = renderer;
|
|
132
|
+
err.frqtlBootedRenderer = booted;
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
if (self._runner) {
|
|
136
|
+
// Restart on the same page replays the same single call: the runner (and its canvas)
|
|
137
|
+
// already ARE that world. A different world needs a fresh page.
|
|
138
|
+
var r = self._runner.canvas_rect;
|
|
139
|
+
if (r.w !== w || r.h !== h || self._runner._depth !== (renderer === 'three' ? d : null) ||
|
|
140
|
+
self._runner._is3D !== (renderer === 'three')) {
|
|
141
|
+
throw new Error('[' + where + '] this page already holds a ' + r.w + 'x' + r.h + ' world; a scene ' +
|
|
142
|
+
'with a different world needs a fresh page. (FAIL-LOUD.)');
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
var ropts = { canvasW: w, canvasH: h, is3D: renderer === 'three' };
|
|
146
|
+
if (renderer === 'three') { ropts.canvasD = d; }
|
|
147
|
+
RUNNER_OPTS.forEach(function (k) { if (self._opts[k] !== undefined) { ropts[k] = self._opts[k]; } });
|
|
148
|
+
self._runner = new FrqtlApp(ropts);
|
|
149
|
+
if (!self._opts.deferCanvas) { self._runner.createCanvas(); }
|
|
150
|
+
}
|
|
151
|
+
var u = self._runner.universe;
|
|
152
|
+
var args = W.initArgs(cfg);
|
|
153
|
+
if (renderer === 'three') {
|
|
154
|
+
args[1].activeAxes = { x: true, y: true, z: true };
|
|
155
|
+
args[1].depth = d;
|
|
156
|
+
}
|
|
157
|
+
// A (re-)inited world has no declared objects; the recipe re-declares what it builds.
|
|
158
|
+
self._runner.clearDeclared();
|
|
159
|
+
W.applyPreInit(cfg, u); // pre-init world keys (iframeIntervalSeconds) — the engine reads them at init
|
|
160
|
+
u.init.apply(u, args);
|
|
161
|
+
self._world = Object.freeze({ renderer: renderer, w: w, h: h, d: renderer === 'three' ? d : null,
|
|
162
|
+
cfg: Object.freeze(Object.assign({}, cfg)) });
|
|
163
|
+
return self;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** p5: a 2D canvas AND a 2D universe of exactly w×h. */
|
|
167
|
+
RecipeContext.prototype.createCanvas = function (w, h, cfg) {
|
|
168
|
+
if (arguments.length > 3) {
|
|
169
|
+
throw new Error('[RecipeContext.createCanvas] takes (w, h, cfg) — a 2D world has no depth; a 3D ' +
|
|
170
|
+
'world is exp.createScene(w, h, d, cfg). (FAIL-LOUD.)');
|
|
171
|
+
}
|
|
172
|
+
return _coinit(this, 'createCanvas', 'p5', w, h, null, cfg);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** three: a 3D scene AND a 3D universe w×h×d (d REQUIRED). */
|
|
176
|
+
RecipeContext.prototype.createScene = function (w, h, d, cfg) {
|
|
177
|
+
return _coinit(this, 'createScene', 'three', w, h, d, cfg);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ── deleted: init(cfg) / initCfg ─────────────────────────────────────────────────
|
|
181
|
+
RecipeContext.prototype.init = function () {
|
|
182
|
+
throw new Error('[RecipeContext] exp.init(cfg) is DELETED — a scene states its world, size included, ' +
|
|
183
|
+
'with ' + COINIT + '. (FAIL-LOUD: a saved module carrying exp.init( must be re-saved.)');
|
|
184
|
+
};
|
|
185
|
+
Object.defineProperty(RecipeContext.prototype, 'initCfg', {
|
|
186
|
+
get: function () {
|
|
187
|
+
throw new Error('[RecipeContext] exp.initCfg is DELETED — read exp.world ({renderer, w, h, d, cfg}). (FAIL-LOUD.)');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── parts, tick-fns, render ──────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
RecipeContext.prototype.addObject = function (kind, config) {
|
|
194
|
+
_requireWorld(this);
|
|
195
|
+
var x = (config && config.x != null) ? config.x : 0;
|
|
196
|
+
var y = (config && config.y != null) ? config.y : 0;
|
|
197
|
+
var ctx = { runner: this._runner, cw: null, x: x, y: y };
|
|
198
|
+
return ObjectFactory.create(kind, config, ctx);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
RecipeContext.prototype.addTickFunction = function (fn) {
|
|
202
|
+
_requireWorld(this);
|
|
203
|
+
var fns = this._runner.universe.experimentTickFunctions;
|
|
204
|
+
fns.push(fn);
|
|
205
|
+
return fns.length - 1;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Name the recipe's LOOK in one call — a pure ferry to `runner.setRenderOptions(bag)`. */
|
|
209
|
+
RecipeContext.prototype.render = function (bag) {
|
|
210
|
+
_requireWorld(this);
|
|
211
|
+
this._runner.setRenderOptions(bag);
|
|
212
|
+
return this;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// ── build / run ──────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Tear down the scene a previous build put on THIS runner, before a same-runner rebuild
|
|
219
|
+
* (`restart()`, a re-applied module on a live page): FAIL-LOUD, never a sweep.
|
|
220
|
+
*
|
|
221
|
+
* OWNERSHIP: `universe.experimentTickFunctions` is the SCENE's (primitive #3) — rebuilt by the
|
|
222
|
+
* replay, so the old scene's entries must all be gone first. `runner.addTickListener` is the
|
|
223
|
+
* HOST's (a recorder, a live chart, a per-tick lerp) and SURVIVES the rebuild untouched.
|
|
224
|
+
*
|
|
225
|
+
* 1. stop the loop;
|
|
226
|
+
* 2. read the scene's UNDECLARED tick-fns (`exp.addTickFunction`) while the registry still
|
|
227
|
+
* says which fns the declared parts own;
|
|
228
|
+
* 3. remove every declared object through `runner.removeObject` — the one per-part path
|
|
229
|
+
* (registry drop → its own dispose() → census check, which THROWS naming a part whose
|
|
230
|
+
* dispose() left a tick-fn or an LO behind);
|
|
231
|
+
* 4. splice the undeclared fns read at step 2;
|
|
232
|
+
* 5. ASSERT the list is empty — a survivor is a tick-fn nothing owned, never cleared for it.
|
|
233
|
+
*/
|
|
234
|
+
function _teardownSceneForRebuild(self) {
|
|
235
|
+
var runner = self._runner;
|
|
236
|
+
runner.stop();
|
|
237
|
+
var fns = runner.universe.experimentTickFunctions;
|
|
238
|
+
var undeclared = runner.undeclaredTickFunctions();
|
|
239
|
+
var declared = runner.declared.slice();
|
|
240
|
+
for (var i = declared.length - 1; i >= 0; i--) { runner.removeObject(declared[i]); }
|
|
241
|
+
for (var j = 0; j < undeclared.length; j++) {
|
|
242
|
+
var at = fns.indexOf(undeclared[j]);
|
|
243
|
+
if (at !== -1) { fns.splice(at, 1); }
|
|
244
|
+
}
|
|
245
|
+
if (fns.length) {
|
|
246
|
+
throw new Error('[RecipeContext.build] rebuilding on the same runner: ' + fns.length + ' tick-function(s) ' +
|
|
247
|
+
'of the previous scene survived its teardown (' + fns.map(function (fn, k) {
|
|
248
|
+
return fn.name ? fn.name : '<anonymous #' + k + '>'; }).join(', ') + ') — no declared part ' +
|
|
249
|
+
'owned them and the scene did not add them (exp.addTickFunction). Host per-tick work belongs ' +
|
|
250
|
+
'on runner.addTickListener, which survives a rebuild. (FAIL-LOUD.)');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Run a recipe WITHOUT starting the loop. Resets `world` first, so a re-run replays the same
|
|
256
|
+
* single co-init call; a recipe that returns without creating its world THROWS. On a runner a
|
|
257
|
+
* previous build already holds, the previous scene is torn down first (_teardownSceneForRebuild:
|
|
258
|
+
* the scene's tick-fns go, the host's tick listeners stay). A successful build RETAINS its
|
|
259
|
+
* replay on the runner (`runner.retainSceneReplay`), so `runner.restart()` — and a sketch's
|
|
260
|
+
* declared `restart` control — replays this build on the same runner and canvas.
|
|
261
|
+
*/
|
|
262
|
+
RecipeContext.prototype.build = function (buildFn) {
|
|
263
|
+
if (buildFn && typeof buildFn === 'function') {
|
|
264
|
+
this._buildFn = buildFn;
|
|
265
|
+
if (this._runner) { _teardownSceneForRebuild(this); }
|
|
266
|
+
this._world = null;
|
|
267
|
+
buildFn(this);
|
|
268
|
+
if (!this._world) {
|
|
269
|
+
throw new Error('[RecipeContext.build] the scene never created its world — its first statement ' +
|
|
270
|
+
'must be ' + COINIT + '. (FAIL-LOUD.)');
|
|
271
|
+
}
|
|
272
|
+
this._built = true;
|
|
273
|
+
var self = this;
|
|
274
|
+
this._runner.retainSceneReplay(function () { return self.build(self._buildFn); });
|
|
275
|
+
}
|
|
276
|
+
setPageExperiment(this);
|
|
277
|
+
return this;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
RecipeContext.prototype.run = function (buildFn) {
|
|
281
|
+
if (buildFn && typeof buildFn === 'function' && !this._built) { this.build(buildFn); }
|
|
282
|
+
_requireWorld(this);
|
|
283
|
+
setPageExperiment(this);
|
|
284
|
+
this._runner.start();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// The world-init contract is NOT a RecipeContext static: reading one of its names THROWS.
|
|
288
|
+
['INIT_CFG', 'assertInitCfg', 'initArgs'].forEach(function (name) {
|
|
289
|
+
Object.defineProperty(RecipeContext, name, {
|
|
290
|
+
get: function () {
|
|
291
|
+
throw new Error('RecipeContext.' + name + ' does not exist: the world-cfg contract is ' +
|
|
292
|
+
'FrqtlWorldInit.' + name + ' (objects/setup/_worldInit.js).');
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
/** Retired facade members → the message reading one throws (compat/engine.js refuseRetiredDoors). */
|
|
298
|
+
var RETIRED_RECIPE_DOORS = Object.freeze({
|
|
299
|
+
buildSource: '[RecipeContext] exp.buildSource(src) is retired — a sketch held as text is a project: boot it ' +
|
|
300
|
+
'with bootProject(mount, { files: { "sketch.js": src }, entry: "sketch.js" }) (@descix/frqtl-web/boot), or ' +
|
|
301
|
+
're-scene a booted facade with exp.build((await linkProject({ files, entry, mint })).buildExperiment) ' +
|
|
302
|
+
'(lib/shim/project/link.js). (FAIL-LOUD.)'
|
|
303
|
+
});
|
|
304
|
+
refuseRetiredDoors(RecipeContext.prototype, RETIRED_RECIPE_DOORS);
|
|
305
|
+
|
|
306
|
+
// ── The page's experiment ─────────────────────────────────────────────────
|
|
307
|
+
// The facade a page built its scene through (or, on the empty page, the sizeless facade it
|
|
308
|
+
// constructed for hosts to apply scenes against). The page door (experiments/_shared/page-door.js)
|
|
309
|
+
// reads it as FrqtlPage.experiment — this is its ONE store.
|
|
310
|
+
var _pageExperiment = null;
|
|
311
|
+
/** @param {RecipeContext} exp */
|
|
312
|
+
function setPageExperiment(exp) {
|
|
313
|
+
if (!(exp instanceof RecipeContext)) {
|
|
314
|
+
throw new TypeError('[RecipeContext] setPageExperiment(exp): expected a RecipeContext. (FAIL-LOUD.)');
|
|
315
|
+
}
|
|
316
|
+
_pageExperiment = exp;
|
|
317
|
+
}
|
|
318
|
+
/** @returns {RecipeContext|null} */
|
|
319
|
+
function pageExperiment() { return _pageExperiment; }
|
|
320
|
+
|
|
321
|
+
export { RecipeContext, RETIRED_RECIPE_DOORS, setPageExperiment, pageExperiment };
|
|
322
|
+
|