@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p1.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 +2 -2
- package/README.md +7 -3
- package/dist/dist-manifest.json +54 -48
- package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
- package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
- package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
- package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
- package/dist/frqtl/objects/FrqtlApp.js +109 -30
- package/dist/frqtl/objects/ObjectFactory.js +4 -5
- package/dist/frqtl/objects/RecipeContext.js +451 -9
- package/dist/frqtl/objects/instruments/hosted-eyes.js +68 -28
- package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
- package/dist/frqtl/objects/schema/index.js +72 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
- package/dist/frqtl/parts/Wall.js +2 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
- package/dist/frqtl/scenes/absorbing-wall.js +18 -14
- package/dist/frqtl/scenes/atomic-model.js +3 -0
- package/dist/frqtl/scenes/big-bang.js +3 -0
- package/dist/frqtl/scenes/blackbody.js +14 -11
- package/dist/frqtl/scenes/circuit-sat.js +7 -4
- package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
- package/dist/frqtl/scenes/counting-wall.js +19 -15
- package/dist/frqtl/scenes/double-slit.js +3 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
- package/dist/frqtl/scenes/dse-one-slit.js +59 -55
- package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
- package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
- package/dist/frqtl/scenes/dse-two-slits.js +69 -65
- package/dist/frqtl/scenes/empty.js +3 -0
- package/dist/frqtl/scenes/laser-buildup.js +23 -16
- package/dist/frqtl/scenes/mirror-3d.js +31 -27
- package/dist/frqtl/scenes/mirror-route.js +4 -1
- package/dist/frqtl/scenes/mirror.js +24 -20
- package/dist/frqtl/scenes/omni-3d.js +7 -4
- package/dist/frqtl/scenes/omni-source.js +7 -4
- package/dist/frqtl/scenes/self-interference.js +13 -10
- package/dist/frqtl/scenes/smoking-gun.js +15 -11
- package/dist/frqtl/scenes/straight-walk.js +7 -4
- package/dist/frqtl/scenes/sun.js +11 -8
- package/dist/frqtl/scenes/two-source.js +22 -17
- package/dist/frqtl/scenes/wave.js +10 -7
- package/dist/shim/bundle/manifest.js +1 -1
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/page-params/index.js +2 -2
- package/dist/shim/project/index.js +7 -5
- package/dist/shim/project/link.js +12 -5
- package/dist/shim/project/parts-module.js +67 -0
- package/dist/shim/project/parts-writer.js +453 -0
- package/dist/shim/project/specifiers.js +41 -13
- package/dist/shim/registry/walkthrough.js +9 -9
- package/dist/shim/render-state/apply.js +11 -14
- package/llms.txt +2 -2
- package/package.json +1 -1
|
@@ -19,12 +19,39 @@
|
|
|
19
19
|
* explicit depth d.
|
|
20
20
|
*
|
|
21
21
|
* exp.world the ONE readable answer: frozen {renderer, w, h, d, cfg}
|
|
22
|
-
* exp.addObject(kind, config) ObjectFactory.create(kind, config, ctx) sugar
|
|
22
|
+
* exp.addObject(kind, config) ObjectFactory.create(kind, config, ctx) sugar (a kind by name or class)
|
|
23
|
+
* exp.declare(parts) the sketch's `export const parts = { name: [Kind, props] }`, in key order;
|
|
24
|
+
* returns the frozen build-time map { key: handle }
|
|
23
25
|
* exp.addTickFunction(fn) universe.experimentTickFunctions.push
|
|
24
26
|
* exp.render(bag) runner.setRenderOptions(bag)
|
|
25
|
-
* exp.build(
|
|
26
|
-
*
|
|
27
|
+
* exp.build(sketch) run the recipe WITHOUT starting the loop; `sketch` is a buildExperiment
|
|
28
|
+
* function or linkProject's result (lib/shim/project/link.js) — then the
|
|
29
|
+
* facade HOLDS that project's text
|
|
30
|
+
* exp.toSketch() write the hands into the held text's `parts` block → { files, changes, refused }
|
|
31
|
+
* exp.setDeclared(name, patch) patch one parts entry and re-run in this document
|
|
27
32
|
* exp.run(buildFn) build (if needed) + start the loop
|
|
33
|
+
*
|
|
34
|
+
* THE WRITER CONTRACT. The facade that linked a sketch is the ONE owner of its current text: the text it
|
|
35
|
+
* linked, then every toSketch / setDeclared result.
|
|
36
|
+
* - exp.declare(parts) calls exp.addObject(Kind, { name: key, ...props }) per own key (zero physics). It
|
|
37
|
+
* refuses a string Kind (naming the import form), an unregistered class, a props.name (the key IS the
|
|
38
|
+
* name) and a second call in one build. Each object it declares is an `inParts` row of runner.declared;
|
|
39
|
+
* its capture right after construction (ObjectSchema.captureProps) is the writer's baseline. It returns
|
|
40
|
+
* the frozen build-time map { key: handle } (`const p = exp.declare(parts); … p.screen`).
|
|
41
|
+
* - toSketch is a HOST call at the END of an act (STUDIO: a hand's release; a lab: the end of a hand),
|
|
42
|
+
* never per placement. It writes, through lib/shim/project/parts-writer.js patchParts, only what a
|
|
43
|
+
* hand did since the last call (the runner's hand mark): an inParts object's touched writable keys
|
|
44
|
+
* (`set`), a declared object added since the build (`add`, the last key, plus one import line), an
|
|
45
|
+
* inParts object no longer live (`remove`). A hand on a code declaration is refused by name. It
|
|
46
|
+
* adopts its result and resets every baseline, so a second call with no hand between is a no-op.
|
|
47
|
+
* - setDeclared validates each key against the kind's descriptor (writable, its own validator),
|
|
48
|
+
* patches the held text and re-runs through the one restart door in THIS document; the seed (the
|
|
49
|
+
* sketch's own co-init cfg) and the camera pose (its poseFields, re-applied through setOrbit) are kept.
|
|
50
|
+
* - A hand is a write through runner.updateObjectProperty or runner.placeDeclaredAt; a host that calls a
|
|
51
|
+
* part's own setter is driving the run, and the writer does not write it. A hand never renames: the key is
|
|
52
|
+
* the name, so a touched `name` is refused (toSketch) or thrown (setDeclared).
|
|
53
|
+
* - setDeclared refuses while a hand is unwritten (await exp.toSketch() first); its `kept` lists what it kept.
|
|
54
|
+
* - restart() of a linked sketch re-links the CURRENT text (the retained replay) and returns a Promise.
|
|
28
55
|
* exp.runner / .universe the runner the co-init call built
|
|
29
56
|
* exp.is3D world.renderer === 'three'
|
|
30
57
|
*
|
|
@@ -43,6 +70,11 @@ import { ObjectFactory } from './ObjectFactory.js';
|
|
|
43
70
|
import { FrqtlWorldInit } from './setup/_worldInit.js';
|
|
44
71
|
import * as FrqtlDisplay from '../compat/renderer/display-slot.js';
|
|
45
72
|
import { refuseRetiredDoors } from '../compat/engine.js';
|
|
73
|
+
import { ObjectSchema } from './schema/index.js';
|
|
74
|
+
import { kindOf } from './kinds.js';
|
|
75
|
+
import { linkProject } from '../../shim/project/link.js';
|
|
76
|
+
import { patchParts, PARTS_EXPORT, entryKeys, entryLiteral } from '../../shim/project/parts-writer.js';
|
|
77
|
+
import { moduleOfKind } from '../../shim/project/parts-module.js';
|
|
46
78
|
|
|
47
79
|
/** The world-init contract (objects/setup/_worldInit.js). */
|
|
48
80
|
function _worldInit() {
|
|
@@ -86,6 +118,11 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
86
118
|
this._world = null; // set by the co-init call; reset by build()
|
|
87
119
|
this._built = false;
|
|
88
120
|
this._buildFn = null;
|
|
121
|
+
this._project = null; // THE held project text: { files: {path: text}, entry, mint, bare? } (build(linked))
|
|
122
|
+
this._urls = null; // the minted URL of each project file, from the last link
|
|
123
|
+
this._parts = new Map(); // declared object → { key, baseline } — the objects in `parts`
|
|
124
|
+
this._code = new Map(); // declared object → baseline — the code declarations at build end
|
|
125
|
+
this._declaredOnce = false;
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
function _requireWorld(self) {
|
|
@@ -148,6 +185,8 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
148
185
|
self._runner = new FrqtlApp(ropts);
|
|
149
186
|
if (!self._opts.deferCanvas) { self._runner.createCanvas(); }
|
|
150
187
|
}
|
|
188
|
+
// The census maps a declared kind's module against the project files this facade linked (FrqtlApp.declare).
|
|
189
|
+
self._runner._projectUrls = self._urls;
|
|
151
190
|
var u = self._runner.universe;
|
|
152
191
|
var args = W.initArgs(cfg);
|
|
153
192
|
if (renderer === 'three') {
|
|
@@ -190,6 +229,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
190
229
|
|
|
191
230
|
// ── parts, tick-fns, render ──────────────────────────────────────────────────────
|
|
192
231
|
|
|
232
|
+
/** exp.addObject(kind, config) — a kind by name (a host's JSON) or by its class. */
|
|
193
233
|
RecipeContext.prototype.addObject = function (kind, config) {
|
|
194
234
|
_requireWorld(this);
|
|
195
235
|
var x = (config && config.x != null) ? config.x : 0;
|
|
@@ -198,6 +238,386 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
198
238
|
return ObjectFactory.create(kind, config, ctx);
|
|
199
239
|
};
|
|
200
240
|
|
|
241
|
+
// ── the parts block ───────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
var IMPORT_FORM = 'import { <Kind> } from \'@descix/frqtl-web/parts\'';
|
|
244
|
+
|
|
245
|
+
/** A deep copy of arrays and plain objects (the module-level literal is never shared with a part). */
|
|
246
|
+
function _plain(v) {
|
|
247
|
+
if (v === null || typeof v !== 'object') { return v; }
|
|
248
|
+
if (Array.isArray(v)) { return v.map(_plain); }
|
|
249
|
+
var proto = Object.getPrototypeOf(v);
|
|
250
|
+
if (proto !== Object.prototype && proto !== null) { return v; }
|
|
251
|
+
var out = {};
|
|
252
|
+
Object.keys(v).forEach(function (k) { out[k] = _plain(v[k]); });
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* exp.declare(parts) — seed the declared parts of `export const parts = { name: [Kind, props], … }`, in key
|
|
258
|
+
* order: `exp.addObject(Kind, { name: key, ...props })` per own key. Packaging, zero physics. Each object
|
|
259
|
+
* declared here is an `inParts` object of the census, and its capture right after construction is the
|
|
260
|
+
* writer's baseline (toSketch diffs against it).
|
|
261
|
+
* RETURNS the build-time map `Object.freeze({ <key>: handle, … })`, in key order — each value the object
|
|
262
|
+
* exp.addObject returned (the same identity as its runner.declared row), so later scene code reads a part
|
|
263
|
+
* as `const p = exp.declare(parts); … p.screen`. A hand add or remove after the build does not update it.
|
|
264
|
+
* REFUSES (throws): a string Kind (name the import form), a Kind that is not a registered class, a
|
|
265
|
+
* `props.name` (the key IS the name), a second declare in one build, an entry not written [Kind, props].
|
|
266
|
+
*/
|
|
267
|
+
RecipeContext.prototype.declare = function (parts) {
|
|
268
|
+
_requireWorld(this);
|
|
269
|
+
if (this._declaredOnce) {
|
|
270
|
+
throw new Error('[RecipeContext.declare] exp.declare(parts) runs ONCE per build — the scene\'s one `export const ' +
|
|
271
|
+
PARTS_EXPORT + '` block is its declared parts. (FAIL-LOUD.)');
|
|
272
|
+
}
|
|
273
|
+
if (!parts || typeof parts !== 'object' || Array.isArray(parts)) {
|
|
274
|
+
throw new TypeError('[RecipeContext.declare] exp.declare(parts): parts is the sketch\'s `export const ' + PARTS_EXPORT +
|
|
275
|
+
' = { name: [Kind, { …props }] }` object. (FAIL-LOUD.)');
|
|
276
|
+
}
|
|
277
|
+
this._declaredOnce = true;
|
|
278
|
+
var self = this;
|
|
279
|
+
var out = {};
|
|
280
|
+
Object.keys(parts).forEach(function (key) {
|
|
281
|
+
var entry = parts[key];
|
|
282
|
+
if (!Array.isArray(entry) || entry.length !== 2 || !entry[1] || typeof entry[1] !== 'object' || Array.isArray(entry[1])) {
|
|
283
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ' is not [Kind, { …props }]. (FAIL-LOUD.)');
|
|
284
|
+
}
|
|
285
|
+
var Kind = entry[0], props = entry[1];
|
|
286
|
+
if (typeof Kind === 'string') {
|
|
287
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ' names its kind by the string "' + Kind + '" — in ' +
|
|
288
|
+
'`parts` a kind is the imported class: ' + IMPORT_FORM.replace('<Kind>', Kind) + '. (exp.addObject(name, ' +
|
|
289
|
+
'props) still takes a string.) (FAIL-LOUD.)');
|
|
290
|
+
}
|
|
291
|
+
if (typeof Kind !== 'function' || !kindOf(Kind)) {
|
|
292
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ': ' + (typeof Kind === 'function'
|
|
293
|
+
? 'the class ' + (Kind.name || '(anonymous)') + ' is not a registered kind in this realm'
|
|
294
|
+
: 'the kind is ' + (Kind === null ? 'null' : typeof Kind) + ', not a registered class') +
|
|
295
|
+
' — ' + IMPORT_FORM + ', or register a forked part\'s renamed class (@descix/frqtl-web/kinds). (FAIL-LOUD.)');
|
|
296
|
+
}
|
|
297
|
+
if (Object.prototype.hasOwnProperty.call(props, 'name')) {
|
|
298
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ' carries name ' + JSON.stringify(props.name) +
|
|
299
|
+
' — in `parts` the KEY is the name ("' + key + '"); delete the name prop. (FAIL-LOUD.)');
|
|
300
|
+
}
|
|
301
|
+
var obj = self.addObject(Kind, Object.assign({ name: key }, _plain(props)));
|
|
302
|
+
self._runner._adoptIntoParts(obj);
|
|
303
|
+
self._parts.set(obj, { key: key, baseline: _capture(self, obj) });
|
|
304
|
+
out[key] = obj;
|
|
305
|
+
});
|
|
306
|
+
return Object.freeze(out);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
function _capture(self, obj) {
|
|
310
|
+
return ObjectSchema.captureProps(obj, self._runner.kindOf(obj));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The fields a hand or setDeclared may write: the descriptor's, minus write-only adapters and readonly reads. */
|
|
314
|
+
function _writable(kind) {
|
|
315
|
+
var d = ObjectSchema.get(kind);
|
|
316
|
+
var out = {};
|
|
317
|
+
((d && d.fields) || []).forEach(function (f) {
|
|
318
|
+
if (f.writeOnly !== true && f.type !== 'readonly') { out[f.name] = f; }
|
|
319
|
+
});
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The writable keys whose live value differs from the baseline (JSON-equal = untouched). */
|
|
324
|
+
function _touched(kind, baseline, live) {
|
|
325
|
+
var w = _writable(kind);
|
|
326
|
+
var out = {};
|
|
327
|
+
var lost = [];
|
|
328
|
+
Object.keys(w).forEach(function (k) {
|
|
329
|
+
var a = baseline[k], b = live[k];
|
|
330
|
+
if (JSON.stringify(a) === JSON.stringify(b)) { return; }
|
|
331
|
+
if (b === undefined) { lost.push(k); return; }
|
|
332
|
+
out[k] = b;
|
|
333
|
+
});
|
|
334
|
+
return { props: out, lost: lost };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
var RENAME = FrqtlApp.RENAME_REFUSAL;
|
|
338
|
+
|
|
339
|
+
var _JSON = function (v) { return JSON.stringify(v); };
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Spell a touched rect in the entry's OWN form (architect z addendum). A 6-tuple rect of a kind whose descriptor
|
|
343
|
+
* derives reads from it (ObjectSchema.derivedReads: Wall `z`, `depth`) is written as the 4-tuple plus each derived
|
|
344
|
+
* key whose value changed, when the entry spells any of those keys as plain properties (entryKeys); otherwise the
|
|
345
|
+
* 6-tuple stands. A 4-tuple equal to the baseline's is not written (no literalizing an untouched expression).
|
|
346
|
+
* Mutates and returns `props`.
|
|
347
|
+
*/
|
|
348
|
+
function _spell(text, name, kind, props, baseline) {
|
|
349
|
+
var derived = ObjectSchema.derivedReads(kind);
|
|
350
|
+
if (!derived.length) { return props; }
|
|
351
|
+
var spelled = entryKeys(text, name);
|
|
352
|
+
var R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
|
|
353
|
+
Object.keys(props).forEach(function (f) {
|
|
354
|
+
var v = props[f];
|
|
355
|
+
var rows = derived.filter(function (d) { return d.field === f && d.tuple === 6 && spelled.indexOf(d.name) !== -1; });
|
|
356
|
+
if (!rows.length || !Array.isArray(v) || v.length !== 6) { return; }
|
|
357
|
+
var b = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f] : null;
|
|
358
|
+
var four = R4.map(function (k) { return v[R6.indexOf(k)]; });
|
|
359
|
+
if (b && _JSON(four) === _JSON(R4.map(function (k) { return b[R6.indexOf(k)]; }))) { delete props[f]; }
|
|
360
|
+
else { props[f] = four; }
|
|
361
|
+
rows.forEach(function (d) {
|
|
362
|
+
var nv = v[R6.indexOf(d.key)];
|
|
363
|
+
if (!b || nv !== b[R6.indexOf(d.key)]) { props[d.name] = nv; }
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
return props;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The post-patch check: what the patched text now declares for entry `name`, read back (entryLiteral) for every
|
|
371
|
+
* written key and folded with the baseline for every unwritten one, must equal the live capture — `rect` in its
|
|
372
|
+
* capture form, through the descriptor's derivedFrom rows. Each mismatch is a refusal naming the key.
|
|
373
|
+
*/
|
|
374
|
+
function _verifyWritten(text, name, kind, written, baseline, live) {
|
|
375
|
+
var refused = [];
|
|
376
|
+
var R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
|
|
377
|
+
var derived = ObjectSchema.derivedReads(kind);
|
|
378
|
+
var decl = {};
|
|
379
|
+
Object.keys(written).forEach(function (k) {
|
|
380
|
+
var r = entryLiteral(text, name, k);
|
|
381
|
+
decl[k] = r.literal ? r.value : undefined;
|
|
382
|
+
});
|
|
383
|
+
function refuse(k, declared, liveV) {
|
|
384
|
+
refused.push({ op: 'set', name: name, key: k, reason: 'the written text declares ' + _JSON(declared) + ' but the live object is ' +
|
|
385
|
+
_JSON(liveV) + ' — the hand is not in the text.' });
|
|
386
|
+
}
|
|
387
|
+
var asDerived = {};
|
|
388
|
+
derived.forEach(function (d) { asDerived[d.name] = d; });
|
|
389
|
+
Object.keys(written).forEach(function (k) {
|
|
390
|
+
if (asDerived[k]) { return; }
|
|
391
|
+
var lv = live[k];
|
|
392
|
+
if (Array.isArray(lv) && lv.length === 6 && derived.some(function (d) { return d.field === k; })) { return; }
|
|
393
|
+
if (_JSON(decl[k]) !== _JSON(lv)) { refuse(k, decl[k], lv); }
|
|
394
|
+
});
|
|
395
|
+
var fields = {};
|
|
396
|
+
derived.forEach(function (d) { fields[d.field] = true; });
|
|
397
|
+
Object.keys(fields).forEach(function (f) {
|
|
398
|
+
var lv = live[f];
|
|
399
|
+
if (!Array.isArray(lv) || lv.length !== 6) { return; }
|
|
400
|
+
var touched = written[f] !== undefined || derived.some(function (d) { return d.field === f && written[d.name] !== undefined; });
|
|
401
|
+
if (!touched) { return; }
|
|
402
|
+
var six = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f].slice() : null;
|
|
403
|
+
var d0 = decl[f];
|
|
404
|
+
if (Array.isArray(d0) && d0.length === 6) { six = d0.slice(); }
|
|
405
|
+
else if (Array.isArray(d0) && d0.length === 4 && six) { R4.forEach(function (k, i) { six[R6.indexOf(k)] = d0[i]; }); }
|
|
406
|
+
derived.forEach(function (d) {
|
|
407
|
+
if (d.field === f && six && decl[d.name] !== undefined) { six[R6.indexOf(d.key)] = decl[d.name]; }
|
|
408
|
+
});
|
|
409
|
+
if (_JSON(six) !== _JSON(lv)) { refuse(f, six, lv); }
|
|
410
|
+
});
|
|
411
|
+
return refused;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
var OUTSIDE_PARTS = 'this sketch declares its parts outside `parts`; STUDIO can run it but cannot write hand edits ' +
|
|
415
|
+
'into it — declare them in `export const parts` to make it editable.';
|
|
416
|
+
|
|
417
|
+
function _requireHeld(self, where) {
|
|
418
|
+
if (!self._project) {
|
|
419
|
+
throw new Error('[RecipeContext.' + where + '] this scene was not built from linked text — the writer writes the ' +
|
|
420
|
+
'text a facade linked: exp.build(await linkProject({ files, entry, mint })) (lib/shim/project/link.js). (FAIL-LOUD.)');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* exp.toSketch() → Promise<{ files, changes, refused }> — write the hands into the sketch's `parts` literal.
|
|
426
|
+
* A HOST call at the END of an act (a drop, a drag end, a delete; a lab's end of a hand), never per
|
|
427
|
+
* placement. For each declared object a hand wrote since the last call (the runner's hand mark: a device
|
|
428
|
+
* door — updateObjectProperty, placeDeclaredAt — wrote it; a part the scene's own code moves is the run,
|
|
429
|
+
* not a hand), it diffs the `inParts` object's live capture against its baseline (touched keys only →
|
|
430
|
+
* `set`), writes each declared object added since the build (`add`, as the last key, plus its import line)
|
|
431
|
+
* and each removed `inParts` object (`remove`), through lib/shim/project/parts-writer.js patchParts on the
|
|
432
|
+
* HELD text. A code-declared object a hand touched (or removed) is refused by name. On return the patched
|
|
433
|
+
* text IS the held text and every baseline is the live capture, so a second call with no hand between
|
|
434
|
+
* returns byte-identical files and no changes.
|
|
435
|
+
* files { path: text } — the whole project as now held
|
|
436
|
+
* changes patchParts' changes (a `literalized` set names the expression it replaced)
|
|
437
|
+
* refused every hand not written, { op, name, key?, reason }
|
|
438
|
+
*/
|
|
439
|
+
RecipeContext.prototype.toSketch = async function () {
|
|
440
|
+
_requireWorld(this);
|
|
441
|
+
_requireHeld(this, 'toSketch');
|
|
442
|
+
var self = this;
|
|
443
|
+
var runner = this._runner;
|
|
444
|
+
var declared = runner.declared;
|
|
445
|
+
var edits = [];
|
|
446
|
+
var refused = [];
|
|
447
|
+
var adopted = [];
|
|
448
|
+
var hands = runner._takeHandTouched();
|
|
449
|
+
this._parts.forEach(function (row, obj) {
|
|
450
|
+
if (declared.indexOf(obj) === -1) { edits.push({ op: 'remove', name: row.key }); return; }
|
|
451
|
+
if (!hands.has(obj)) { return; }
|
|
452
|
+
var kind = runner.kindOf(obj);
|
|
453
|
+
var t = _touched(kind, row.baseline, _capture(self, obj));
|
|
454
|
+
if (Object.prototype.hasOwnProperty.call(t.props, 'name')) {
|
|
455
|
+
refused.push({ op: 'set', name: row.key, key: 'name', reason: RENAME });
|
|
456
|
+
delete t.props.name;
|
|
457
|
+
}
|
|
458
|
+
t.lost.forEach(function (k) {
|
|
459
|
+
refused.push({ op: 'set', name: row.key, key: k, reason: 'the live object no longer reports "' + k + '" — there is no value to write.' });
|
|
460
|
+
});
|
|
461
|
+
if (Object.keys(t.props).length) {
|
|
462
|
+
_spell(self._project.files[self._project.entry], row.key, kind, t.props, row.baseline);
|
|
463
|
+
if (Object.keys(t.props).length) { edits.push({ op: 'set', name: row.key, props: t.props, obj: obj, kind: kind, baseline: row.baseline }); }
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
this._code.forEach(function (baseline, obj) {
|
|
467
|
+
var name = obj.name || runner.kindOf(obj);
|
|
468
|
+
if (declared.indexOf(obj) === -1) {
|
|
469
|
+
refused.push({ op: 'remove', name: name, reason: '"' + name + '" is declared in code: ' + OUTSIDE_PARTS });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!hands.has(obj)) { return; }
|
|
473
|
+
var t = _touched(runner.kindOf(obj), baseline, _capture(self, obj));
|
|
474
|
+
if (Object.keys(t.props).length || t.lost.length) {
|
|
475
|
+
refused.push({ op: 'set', name: name, reason: '"' + name + '" is declared in code: ' + OUTSIDE_PARTS });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
declared.forEach(function (obj) {
|
|
479
|
+
if (self._parts.has(obj) || self._code.has(obj)) { return; }
|
|
480
|
+
var kind = runner.kindOf(obj);
|
|
481
|
+
var row = kindOf(kind);
|
|
482
|
+
var props = _capture(self, obj);
|
|
483
|
+
var w = _writable(kind);
|
|
484
|
+
Object.keys(props).forEach(function (k) { if (!w[k] || k === 'name') { delete props[k]; } });
|
|
485
|
+
edits.push({ op: 'add', name: obj.name || kind, Kind: kind, module: row ? moduleOfKind(row.from, self._urls) : null, props: props });
|
|
486
|
+
adopted.push(obj);
|
|
487
|
+
});
|
|
488
|
+
var entry = this._project.entry;
|
|
489
|
+
var r = patchParts(this._project.files[entry], edits.map(function (e) {
|
|
490
|
+
return e.op === 'set' ? { op: 'set', name: e.name, props: e.props } : e;
|
|
491
|
+
}), { entry: entry });
|
|
492
|
+
// Never decide it succeeded without asking: read the patched text back against each moved object.
|
|
493
|
+
edits.forEach(function (e) {
|
|
494
|
+
if (e.op !== 'set') { return; }
|
|
495
|
+
if (r.refused.some(function (x) { return x.op === 'set' && x.name === e.name; })) { return; }
|
|
496
|
+
refused.push.apply(refused, _verifyWritten(r.text, e.name, e.kind, e.props, e.baseline, _capture(self, e.obj)));
|
|
497
|
+
});
|
|
498
|
+
var refusedAdd = {};
|
|
499
|
+
r.refused.forEach(function (x) { if (x.op === 'add') { refusedAdd[x.name] = true; } });
|
|
500
|
+
var files = Object.assign({}, this._project.files);
|
|
501
|
+
files[entry] = r.text;
|
|
502
|
+
this._project = Object.assign({}, this._project, { files: files });
|
|
503
|
+
// Adopt: the written text is held; baselines follow the live objects.
|
|
504
|
+
var removedRefused = {};
|
|
505
|
+
r.refused.forEach(function (x) { if (x.op === 'remove') { removedRefused[x.name] = true; } });
|
|
506
|
+
this._parts.forEach(function (row, obj) {
|
|
507
|
+
if (declared.indexOf(obj) === -1) { if (!removedRefused[row.key]) { self._parts.delete(obj); } return; }
|
|
508
|
+
row.baseline = _capture(self, obj);
|
|
509
|
+
});
|
|
510
|
+
adopted.forEach(function (obj) {
|
|
511
|
+
var key = obj.name || runner.kindOf(obj);
|
|
512
|
+
if (refusedAdd[key]) { return; }
|
|
513
|
+
runner._adoptIntoParts(obj);
|
|
514
|
+
self._parts.set(obj, { key: key, baseline: _capture(self, obj) });
|
|
515
|
+
});
|
|
516
|
+
return { files: Object.assign({}, files), changes: r.changes, refused: refused.concat(r.refused) };
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/** The names of declared objects a hand marked whose live capture differs from their baseline (not yet written). */
|
|
520
|
+
function _unwrittenHands(self) {
|
|
521
|
+
var runner = self._runner;
|
|
522
|
+
var marks = runner._handTouched;
|
|
523
|
+
if (!marks || !marks.size) { return []; }
|
|
524
|
+
var out = [];
|
|
525
|
+
marks.forEach(function (obj) {
|
|
526
|
+
if (runner.declared.indexOf(obj) === -1) { return; }
|
|
527
|
+
var row = self._parts.get(obj);
|
|
528
|
+
var baseline = row ? row.baseline : self._code.get(obj);
|
|
529
|
+
if (!baseline) { return; }
|
|
530
|
+
var t = _touched(runner.kindOf(obj), baseline, _capture(self, obj));
|
|
531
|
+
if (Object.keys(t.props).length || t.lost.length) { out.push(row ? row.key : (obj.name || runner.kindOf(obj))); }
|
|
532
|
+
});
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** The camera pose the runner's camera declares (poseFields), or null with no camera. */
|
|
537
|
+
function _cameraPose(runner) {
|
|
538
|
+
var cam = runner.cameraControl;
|
|
539
|
+
if (!cam || typeof cam.getOrbit !== 'function' || !Array.isArray(cam.poseFields)) { return null; }
|
|
540
|
+
var orbit = cam.getOrbit() || {};
|
|
541
|
+
var pose = {};
|
|
542
|
+
cam.poseFields.forEach(function (f) { if (orbit[f.name] !== undefined) { pose[f.name] = _plain(orbit[f.name]); } });
|
|
543
|
+
return pose;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* exp.setDeclared(name, patch) → Promise<{ set, from, to, tick: 0, kept, files }> — change
|
|
548
|
+
* an `inParts` object's declaration and re-run the scene IN THIS DOCUMENT. `patch` keys must be writable fields
|
|
549
|
+
* of the kind's descriptor and each value must pass the descriptor's own validator (ObjectSchema.normalizeValue);
|
|
550
|
+
* otherwise it refuses (throws) naming the key. The held text is patched (patchParts `set`), then the one restart
|
|
551
|
+
* door (runner.restart → the retained replay re-links the held text) re-runs it: the seed (the sketch's own
|
|
552
|
+
* createScene/createCanvas) and the camera pose (captured through the camera's poseFields, re-applied through
|
|
553
|
+
* setOrbit) are kept; `kept` lists 'cameraPose' only when a pose was captured and re-applied. A `rect` /
|
|
554
|
+
* `center` patch sets where the entry STARTS. The loop's run state is kept. REFUSES (throws) while a hand
|
|
555
|
+
* made since the last toSketch is unwritten (write it first: await exp.toSketch()), and a `name` patch (the
|
|
556
|
+
* key is the name).
|
|
557
|
+
*/
|
|
558
|
+
RecipeContext.prototype.setDeclared = async function (name, patch) {
|
|
559
|
+
_requireWorld(this);
|
|
560
|
+
_requireHeld(this, 'setDeclared');
|
|
561
|
+
var runner = this._runner;
|
|
562
|
+
var obj = null;
|
|
563
|
+
this._parts.forEach(function (row, o) { if (row.key === name) { obj = o; } });
|
|
564
|
+
if (!obj || runner.declared.indexOf(obj) === -1) {
|
|
565
|
+
throw new Error('[RecipeContext.setDeclared] "' + name + '" is not an object of this sketch\'s `parts` block' +
|
|
566
|
+
(this._code.size ? ' (code declarations: ' + Array.from(this._code.keys()).map(function (o) { return o.name; }).join(', ') +
|
|
567
|
+
' — ' + OUTSIDE_PARTS + ')' : '') + '. (FAIL-LOUD.)');
|
|
568
|
+
}
|
|
569
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch) || !Object.keys(patch).length) {
|
|
570
|
+
throw new TypeError('[RecipeContext.setDeclared] patch is { <field>: value, … } with at least one field. (FAIL-LOUD.)');
|
|
571
|
+
}
|
|
572
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'name')) {
|
|
573
|
+
throw new TypeError('[RecipeContext.setDeclared] "name": ' + RENAME + ' (FAIL-LOUD.)');
|
|
574
|
+
}
|
|
575
|
+
// A hand made since the last toSketch is not in the held text, and the re-run would drop it without a
|
|
576
|
+
// word: refuse, naming the object, until the host writes it (a placement and a setDeclared are two changes).
|
|
577
|
+
var unwritten = _unwrittenHands(this);
|
|
578
|
+
if (unwritten.length) {
|
|
579
|
+
throw new Error('[RecipeContext.setDeclared] "' + unwritten.join('", "') + '" carries a hand the sketch text does ' +
|
|
580
|
+
'not hold yet — write the hand first: await exp.toSketch(). (FAIL-LOUD.)');
|
|
581
|
+
}
|
|
582
|
+
var kind = runner.kindOf(obj);
|
|
583
|
+
var w = _writable(kind);
|
|
584
|
+
var live = _capture(this, obj);
|
|
585
|
+
var from = {};
|
|
586
|
+
Object.keys(patch).forEach(function (k) {
|
|
587
|
+
if (!w[k]) {
|
|
588
|
+
throw new TypeError('[RecipeContext.setDeclared] "' + k + '" is not a writable field of ' + kind + ' (writable: ' +
|
|
589
|
+
Object.keys(w).join(', ') + '). (FAIL-LOUD.)');
|
|
590
|
+
}
|
|
591
|
+
try { ObjectSchema.normalizeValue(w[k], patch[k], kind); }
|
|
592
|
+
catch (err) {
|
|
593
|
+
throw new TypeError('[RecipeContext.setDeclared] "' + k + '": ' + String((err && err.message) || err) + ' (FAIL-LOUD.)');
|
|
594
|
+
}
|
|
595
|
+
from[k] = live[k] === undefined ? null : live[k];
|
|
596
|
+
});
|
|
597
|
+
var entry = this._project.entry;
|
|
598
|
+
var spelled = _spell(this._project.files[entry], name, kind, _plain(patch), live);
|
|
599
|
+
var r = patchParts(this._project.files[entry], [{ op: 'set', name: name, props: spelled }], { entry: entry });
|
|
600
|
+
if (r.refused.length) {
|
|
601
|
+
throw new Error('[RecipeContext.setDeclared] the sketch text refused the patch: ' +
|
|
602
|
+
r.refused.map(function (x) { return (x.key ? x.key + ': ' : '') + x.reason; }).join('; ') + ' (FAIL-LOUD.)');
|
|
603
|
+
}
|
|
604
|
+
var files = Object.assign({}, this._project.files);
|
|
605
|
+
files[entry] = r.text;
|
|
606
|
+
this._project = Object.assign({}, this._project, { files: files });
|
|
607
|
+
var pose = _cameraPose(runner);
|
|
608
|
+
var wasRunning = !!runner._isLooping;
|
|
609
|
+
await runner.restart();
|
|
610
|
+
if (!wasRunning) { runner.stop(); }
|
|
611
|
+
// `kept` is what WAS kept: the seed by construction (the sketch's own co-init cfg); the camera pose only
|
|
612
|
+
// when one was captured and re-applied (a 2D scene with no pose camera has none).
|
|
613
|
+
var kept = ['seed'];
|
|
614
|
+
if (pose && runner.cameraControl && typeof runner.cameraControl.setOrbit === 'function') {
|
|
615
|
+
runner.cameraControl.setOrbit(pose);
|
|
616
|
+
kept.push('cameraPose');
|
|
617
|
+
}
|
|
618
|
+
return { set: name, from: from, to: _plain(patch), tick: 0, kept: kept, files: Object.assign({}, files) };
|
|
619
|
+
};
|
|
620
|
+
|
|
201
621
|
RecipeContext.prototype.addTickFunction = function (fn) {
|
|
202
622
|
_requireWorld(this);
|
|
203
623
|
var fns = this._runner.universe.experimentTickFunctions;
|
|
@@ -259,19 +679,41 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
259
679
|
* replay on the runner (`runner.retainSceneReplay`), so `runner.restart()` — and a sketch's
|
|
260
680
|
* declared `restart` control — replays this build on the same runner and canvas.
|
|
261
681
|
*/
|
|
262
|
-
RecipeContext.prototype.build = function (
|
|
263
|
-
|
|
264
|
-
|
|
682
|
+
RecipeContext.prototype.build = function (sketch) {
|
|
683
|
+
var linked = null;
|
|
684
|
+
if (sketch && typeof sketch === 'object' && typeof sketch.buildExperiment === 'function') {
|
|
685
|
+
if (!sketch.project || typeof sketch.project !== 'object' || !sketch.project.files || typeof sketch.project.entry !== 'string') {
|
|
686
|
+
throw new TypeError('[RecipeContext.build] build(linked): linked is linkProject\'s result { buildExperiment, files, ' +
|
|
687
|
+
'urls, project } (lib/shim/project/link.js). (FAIL-LOUD.)');
|
|
688
|
+
}
|
|
689
|
+
linked = sketch;
|
|
690
|
+
sketch = linked.buildExperiment;
|
|
691
|
+
} else if (sketch !== undefined && sketch !== null && typeof sketch !== 'function') {
|
|
692
|
+
throw new TypeError('[RecipeContext.build] build(sketch): sketch is the scene\'s buildExperiment(exp) function or ' +
|
|
693
|
+
'linkProject\'s result (got ' + typeof sketch + '). (FAIL-LOUD.)');
|
|
694
|
+
}
|
|
695
|
+
if (typeof sketch === 'function') {
|
|
696
|
+
this._buildFn = sketch;
|
|
697
|
+
this._project = linked ? linked.project : null;
|
|
698
|
+
this._urls = linked ? (linked.urls || null) : null;
|
|
265
699
|
if (this._runner) { _teardownSceneForRebuild(this); }
|
|
266
700
|
this._world = null;
|
|
267
|
-
|
|
701
|
+
this._parts = new Map();
|
|
702
|
+
this._code = new Map();
|
|
703
|
+
this._declaredOnce = false;
|
|
704
|
+
sketch(this);
|
|
268
705
|
if (!this._world) {
|
|
269
706
|
throw new Error('[RecipeContext.build] the scene never created its world — its first statement ' +
|
|
270
707
|
'must be ' + COINIT + '. (FAIL-LOUD.)');
|
|
271
708
|
}
|
|
272
709
|
this._built = true;
|
|
273
710
|
var self = this;
|
|
274
|
-
this._runner.
|
|
711
|
+
this._runner.declared.forEach(function (obj) {
|
|
712
|
+
if (!self._parts.has(obj)) { self._code.set(obj, _capture(self, obj)); }
|
|
713
|
+
});
|
|
714
|
+
this._runner.retainSceneReplay(this._project
|
|
715
|
+
? function () { return linkProject(self._project).then(function (l) { return self.build(l); }); }
|
|
716
|
+
: function () { return self.build(self._buildFn); });
|
|
275
717
|
}
|
|
276
718
|
setPageExperiment(this);
|
|
277
719
|
return this;
|
|
@@ -298,7 +740,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
298
740
|
var RETIRED_RECIPE_DOORS = Object.freeze({
|
|
299
741
|
buildSource: '[RecipeContext] exp.buildSource(src) is retired — a sketch held as text is a project: boot it ' +
|
|
300
742
|
'with bootProject(mount, { files: { "sketch.js": src }, entry: "sketch.js" }) (@descix/frqtl-web/boot), or ' +
|
|
301
|
-
're-scene a booted facade with exp.build(
|
|
743
|
+
're-scene a booted facade with exp.build(await linkProject({ files, entry, mint })) ' +
|
|
302
744
|
'(lib/shim/project/link.js). (FAIL-LOUD.)'
|
|
303
745
|
});
|
|
304
746
|
refuseRetiredDoors(RecipeContext.prototype, RETIRED_RECIPE_DOORS);
|