@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.
Files changed (58) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +7 -3
  3. package/dist/dist-manifest.json +54 -48
  4. package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
  5. package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
  6. package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
  7. package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
  8. package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
  9. package/dist/frqtl/objects/FrqtlApp.js +109 -30
  10. package/dist/frqtl/objects/ObjectFactory.js +4 -5
  11. package/dist/frqtl/objects/RecipeContext.js +451 -9
  12. package/dist/frqtl/objects/instruments/hosted-eyes.js +68 -28
  13. package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
  14. package/dist/frqtl/objects/schema/index.js +72 -0
  15. package/dist/frqtl/parts/DetectorWall.js +1 -1
  16. package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
  17. package/dist/frqtl/parts/Wall.js +2 -0
  18. package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
  19. package/dist/frqtl/scenes/absorbing-wall.js +18 -14
  20. package/dist/frqtl/scenes/atomic-model.js +3 -0
  21. package/dist/frqtl/scenes/big-bang.js +3 -0
  22. package/dist/frqtl/scenes/blackbody.js +14 -11
  23. package/dist/frqtl/scenes/circuit-sat.js +7 -4
  24. package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
  25. package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
  26. package/dist/frqtl/scenes/counting-wall.js +19 -15
  27. package/dist/frqtl/scenes/double-slit.js +3 -0
  28. package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
  29. package/dist/frqtl/scenes/dse-one-slit.js +59 -55
  30. package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
  31. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
  32. package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
  33. package/dist/frqtl/scenes/dse-two-slits.js +69 -65
  34. package/dist/frqtl/scenes/empty.js +3 -0
  35. package/dist/frqtl/scenes/laser-buildup.js +23 -16
  36. package/dist/frqtl/scenes/mirror-3d.js +31 -27
  37. package/dist/frqtl/scenes/mirror-route.js +4 -1
  38. package/dist/frqtl/scenes/mirror.js +24 -20
  39. package/dist/frqtl/scenes/omni-3d.js +7 -4
  40. package/dist/frqtl/scenes/omni-source.js +7 -4
  41. package/dist/frqtl/scenes/self-interference.js +13 -10
  42. package/dist/frqtl/scenes/smoking-gun.js +15 -11
  43. package/dist/frqtl/scenes/straight-walk.js +7 -4
  44. package/dist/frqtl/scenes/sun.js +11 -8
  45. package/dist/frqtl/scenes/two-source.js +22 -17
  46. package/dist/frqtl/scenes/wave.js +10 -7
  47. package/dist/shim/bundle/manifest.js +1 -1
  48. package/dist/shim/engine/router.js +2 -2
  49. package/dist/shim/page-params/index.js +2 -2
  50. package/dist/shim/project/index.js +7 -5
  51. package/dist/shim/project/link.js +12 -5
  52. package/dist/shim/project/parts-module.js +67 -0
  53. package/dist/shim/project/parts-writer.js +453 -0
  54. package/dist/shim/project/specifiers.js +41 -13
  55. package/dist/shim/registry/walkthrough.js +9 -9
  56. package/dist/shim/render-state/apply.js +11 -14
  57. package/llms.txt +2 -2
  58. 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(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.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 (buildFn) {
263
- if (buildFn && typeof buildFn === 'function') {
264
- this._buildFn = buildFn;
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
- buildFn(this);
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.retainSceneReplay(function () { return self.build(self._buildFn); });
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((await linkProject({ files, entry, mint })).buildExperiment) ' +
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);