@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
@@ -28,6 +28,16 @@
28
28
  * z absent → [cx − w/2, cy − h/2, w, h], and cz must be 0 — a z-less rect sits at z-centre
29
29
  * 0 by the rule above, so any other cz THROWS rather than being dropped.
30
30
  *
31
+ * A HAND places on the pixel lattice (frames sit on integer pixels; a hand between pixels is a
32
+ * screen-scale artifact), so every hand write is snapped once, here:
33
+ *
34
+ * snapRectPosition(t) → a rect write tuple with its POSITION on the lattice: `Math.round` of left,
35
+ * top (and z of a 6-tuple); the size (w, h, d) is returned byte-identical — a hand moves, it never
36
+ * resizes.
37
+ * snapCenter(c) → a centre ([x, y] / [x, y, z]) with every component rounded.
38
+ * Both are called by the hand doors only (FrqtlApp.placeDeclaredAt; STUDIO's drag / resize write). A
39
+ * value typed into a field is the user's literal and is never snapped; no build path calls them.
40
+ *
31
41
  * ── The drawn VOLUME and the center-ray hit (STUDIO's aim) ──────────────────────
32
42
  *
33
43
  * The same rect also owns the VOLUME the aim ray tests and the shadow-surround paints, so the
@@ -94,6 +104,7 @@ function rectAtCenter(rect, center) {
94
104
  var DEG = Math.PI / 180;
95
105
 
96
106
  /** The rect's z-extent: d ?? w when it carries z (the engine's z-capable rectangle), else 0. */
107
+ function zExtent(rect) { return _zExtent(rect); }
97
108
  function _zExtent(rect) {
98
109
  if (!hasZ(rect)) { return 0; }
99
110
  return (typeof rect.d === 'number') ? rect.d : rect.w;
@@ -220,9 +231,39 @@ function firstHit(origin, dir, volumes, margin) {
220
231
  return best;
221
232
  }
222
233
 
234
+ function _finiteTuple(v, who, lengths) {
235
+ if (!Array.isArray(v) || lengths.indexOf(v.length) === -1 ||
236
+ !v.every(function (n) { return typeof n === 'number' && isFinite(n); })) {
237
+ throw new TypeError('[LoDrawnCenter.' + who + '] ' + lengths.join(' or ') + ' finite numbers; got ' +
238
+ JSON.stringify(v) + '. (FAIL-LOUD.)');
239
+ }
240
+ }
241
+
242
+ /**
243
+ * snapRectPosition(t) — a hand's rect write with its position on the pixel lattice, its size untouched.
244
+ * @param {number[]} t — [left, top, w, h] or [left, top, z, w, h, d] (RECT_TUPLE order) @returns {number[]}
245
+ */
246
+ function snapRectPosition(t) {
247
+ _finiteTuple(t, 'snapRectPosition', [4, 6]);
248
+ var npos = t.length === 6 ? 3 : 2;
249
+ return t.map(function (n, i) { return i < npos ? Math.round(n) : n; });
250
+ }
251
+
252
+ /**
253
+ * snapCenter(c) — a hand's centre write on the pixel lattice: every component rounded.
254
+ * @param {number[]} c — [x, y] or [x, y, z] @returns {number[]}
255
+ */
256
+ function snapCenter(c) {
257
+ _finiteTuple(c, 'snapCenter', [2, 3]);
258
+ return c.map(function (n) { return Math.round(n); });
259
+ }
260
+
223
261
  export {
224
262
  drawnCenter,
225
263
  rectAtCenter,
264
+ snapRectPosition,
265
+ snapCenter,
266
+ zExtent,
226
267
  hasZ,
227
268
  drawnVolume,
228
269
  rayIntersect,
@@ -0,0 +1,60 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * boot-failure.js — THE one boot-failure overlay painter.
5
+ *
6
+ * paintBootFailure(axis, msg) paint a FAIL-LOUD overlay for a boot axis ('engine' | 'renderer')
7
+ * setBootOverlayHost(el) scope later overlays into `el` (the boot's `overlayHost`)
8
+ *
9
+ * A leaf: the boot module (boot.js) and any runner that discovers a boot fault after the boot
10
+ * (FrqtlApp's empty-display-slot diagnosis) paint through this ONE painter.
11
+ */
12
+
13
+ const AXIS_STYLE = {
14
+ engine: { id: 'egpt-engine-error', marker: 'data-egpt-engine-error', bg: '#1a0000', fg: '#ff6b6b', border: '#b6161c' },
15
+ renderer: { id: 'egpt-renderer-error', marker: 'data-egpt-renderer-error', bg: '#000a1a', fg: '#6b9eff', border: '#1640b6' }
16
+ };
17
+ let _overlayHost = null;
18
+
19
+ /** The element the next overlays paint into (null = the full viewport) — set by the boot from its opts. */
20
+ export function setBootOverlayHost(el) { _overlayHost = el || null; }
21
+
22
+ /**
23
+ * THE boot-failure overlay: scoped into the boot's overlay host when one was given, else the full
24
+ * viewport. One overlay per axis (a second failure on the same axis does not stack). Console-loud too.
25
+ *
26
+ * @param {'engine'|'renderer'} axis 'engine' covers every non-renderer boot failure (engine, game layer, sketch)
27
+ * @param {string} msg
28
+ */
29
+ export function paintBootFailure(axis, msg) {
30
+ const st = AXIS_STYLE[axis];
31
+ if (!st) { throw new Error('[boot] paintBootFailure: axis must be engine | renderer (got ' + JSON.stringify(axis) + ')'); }
32
+ if (typeof console !== 'undefined' && console.error) { console.error(msg); }
33
+ function paint() {
34
+ if (_overlayHost) {
35
+ if (_overlayHost.querySelector && _overlayHost.querySelector('[' + st.marker + ']')) { return; }
36
+ const b = document.createElement('div');
37
+ b.setAttribute(st.marker, '1');
38
+ b.setAttribute('data-egpt-boot-error', '1');
39
+ b.style.cssText = 'position:absolute;inset:0;z-index:9998;background:' + st.bg + ';color:' + st.fg + ';' +
40
+ 'font:12px/1.5 ui-monospace,Menlo,Consolas,monospace;padding:14px;white-space:pre-wrap;overflow:auto;' +
41
+ 'border:2px solid ' + st.border + ';';
42
+ b.textContent = msg;
43
+ const cs = getComputedStyle(_overlayHost);
44
+ if (!cs || cs.position === 'static') { _overlayHost.style.position = 'relative'; }
45
+ _overlayHost.appendChild(b);
46
+ return;
47
+ }
48
+ if (document.getElementById(st.id)) { return; }
49
+ const b = document.createElement('div');
50
+ b.id = st.id;
51
+ b.setAttribute('data-egpt-boot-error', '1');
52
+ b.style.cssText = 'position:fixed;inset:0;z-index:99999;background:' + st.bg + ';color:' + st.fg + ';' +
53
+ 'font:13px/1.55 ui-monospace,Menlo,Consolas,monospace;padding:22px;white-space:pre-wrap;overflow:auto;' +
54
+ 'border:2px solid ' + st.border + ';';
55
+ b.textContent = msg;
56
+ (document.body || document.documentElement).appendChild(b);
57
+ }
58
+ if (document.body) { paint(); } else { addEventListener('DOMContentLoaded', paint); }
59
+ }
60
+
@@ -62,6 +62,8 @@
62
62
  */
63
63
 
64
64
  import { ObjectSchema } from './schema/index.js';
65
+ import { kindOf as _kindRow } from './kinds.js';
66
+ import { moduleOfKind } from '../../shim/project/parts-module.js';
65
67
  import { FrqtlCapture } from './instruments/capture.js';
66
68
  import * as FrqtlDisplay from '../compat/renderer/display-slot.js';
67
69
  import * as RendererProviderContract from '../compat/renderer/renderer-provider-contract.js';
@@ -867,9 +869,19 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
867
869
  * THROWS here instead of leaking an unresolvable LO into the scene. `tickFns` are the host
868
870
  * tick-functions the build pushed (an emitter's emit fn, an absorb radiator): what the
869
871
  * declaration owns besides what it draws. THROWS on a re-declaration.
870
- * @param {Object} handle @param {Array} members @param {Array} [tickFns] @returns {Object} handle
871
- */
872
- FrqtlApp.prototype.declare = function (handle, members, tickFns) {
872
+ *
873
+ * `row` = { kind } (ObjectFactory.create): the census fields every declared object carries, read as
874
+ * `d.inParts` / `d.module` off a runner.declared entry (not enumerable, not writable):
875
+ * inParts — true when the object came from the sketch's `export const parts` block (exp.declare),
876
+ * or was written into it since (RecipeContext.toSketch); false for a code declaration.
877
+ * Set only by RecipeContext, the parts block's one owner (_adoptIntoParts)
878
+ * module — the specifier a sketch imports its kind by (lib/shim/project/parts-module.js
879
+ * moduleOfKind over the kind row's `from` and the project files the facade last linked):
880
+ * '@descix/frqtl-web/parts', a project path, or null (an internal kind)
881
+ * @param {Object} handle @param {Array} members @param {Array} [tickFns] @param {{kind: string}} [row]
882
+ * @returns {Object} handle
883
+ */
884
+ FrqtlApp.prototype.declare = function (handle, members, tickFns, row) {
873
885
  if (!handle || typeof handle !== 'object') {
874
886
  throw new TypeError('[FrqtlApp.declare] a declared object is a handle, got ' + typeof handle + '. (FAIL-LOUD.)');
875
887
  }
@@ -893,15 +905,62 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
893
905
  'object. (FAIL-LOUD.)');
894
906
  }
895
907
  }
908
+ var kind = (row && row.kind) || handle._schemaKind;
909
+ var kr = kind ? _kindRow(kind) : null;
910
+ _setCensusRow(handle, {
911
+ inParts: false,
912
+ module: kr ? moduleOfKind(kr.from, this._projectUrls || null) : null
913
+ });
896
914
  return handle;
897
915
  };
898
916
 
917
+ // The census fields of each declared object (FrqtlApp.declare). Keyed by the handle; read through two
918
+ // getters defined on it, so `runner.declared[i].inParts` / `.module` are fields of the one census.
919
+ var _censusRows = new WeakMap();
920
+ function _setCensusRow(handle, r) {
921
+ _censusRows.set(handle, r);
922
+ if (!Object.prototype.hasOwnProperty.call(handle, 'inParts')) {
923
+ Object.defineProperty(handle, 'inParts', { configurable: true, enumerable: false,
924
+ get: function () { var x = _censusRows.get(this); return x ? x.inParts : false; } });
925
+ Object.defineProperty(handle, 'module', { configurable: true, enumerable: false,
926
+ get: function () { var x = _censusRows.get(this); return x ? x.module : null; } });
927
+ }
928
+ }
929
+
930
+ // THE HAND MARK: a declared object a device door wrote since the writer last ran (updateObjectProperty —
931
+ // a drag, a resize, an Inspector or knob write — and placeDeclaredAt — a carry, a lab's hand). The writer
932
+ // (RecipeContext.toSketch) writes, or refuses, ONLY marked objects: a part its own scene code moves (a
933
+ // bullet's flight tick-function) is the run, not a hand.
934
+ function _markHand(app, x) {
935
+ var list = app._declared;
936
+ if (!list || !list.length) { return; } // nothing declared: no object a writer could write
937
+ var d = app.declaredOf(x);
938
+ if (d) { (app._handTouched || (app._handTouched = new Set())).add(d); }
939
+ }
940
+ /** _takeHandTouched() → the declared objects a device door wrote since the last call; the call clears the mark. */
941
+ FrqtlApp.prototype._takeHandTouched = function () {
942
+ var out = this._handTouched || new Set();
943
+ this._handTouched = new Set();
944
+ return out;
945
+ };
946
+
947
+ /**
948
+ * _adoptIntoParts(d) — `d` is an entry of the parts block (its key is its name): declared there (exp.declare)
949
+ * or written there (RecipeContext.toSketch). RecipeContext's alone, the parts block's one owner.
950
+ */
951
+ FrqtlApp.prototype._adoptIntoParts = function (d) {
952
+ var x = _censusRows.get(d);
953
+ if (!x) { throw new Error('[FrqtlApp] _adoptIntoParts: "' + (d && d.name) + '" is not a declared object. (FAIL-LOUD.)'); }
954
+ x.inParts = true;
955
+ };
956
+
899
957
  /** clearDeclared() — empty the registry. Called where the world re-inits (FrqtlApp.setup, and the
900
958
  * RecipeContext co-init exp.createCanvas / exp.createScene): a fresh world has no declared objects. */
901
959
  FrqtlApp.prototype.clearDeclared = function () {
902
960
  this._declared = [];
903
961
  this._declaredMembers = new Map();
904
962
  this._declaredTicks = new Map();
963
+ this._handTouched = new Set(); // a fresh world holds no hand
905
964
  };
906
965
 
907
966
  /**
@@ -1077,6 +1136,14 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1077
1136
  */
1078
1137
  FrqtlApp.prototype.updateObjectProperty = function (lo, prop, value) {
1079
1138
  if (!lo) return;
1139
+ // A parts entry's name IS its key: no door renames it (toSketch refuses, setDeclared throws, and so does this),
1140
+ // whether addressed by its declared handle or by a member LO of it (declaredOf walks the owner chain).
1141
+ var entry = prop === 'name' ? this.declaredOf(lo) : null;
1142
+ if (entry && entry.inParts) {
1143
+ throw new TypeError('[FrqtlApp.updateObjectProperty] "name" of the parts entry "' + entry.name + '": ' +
1144
+ FrqtlApp.RENAME_REFUSAL + ' (FAIL-LOUD.)');
1145
+ }
1146
+ _markHand(this, lo);
1080
1147
 
1081
1148
  // Every shipped object class STAMPS `_schemaKind` (LargeObject, QuantumEmitter,
1082
1149
  // PointSource, Wall, DetectorWall, SphereEmitter, …), so the
@@ -1443,6 +1510,7 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1443
1510
  * where the object now IS (`centerOf`).
1444
1511
  */
1445
1512
  FrqtlApp.prototype.placeDeclaredAt = function (d, point) {
1513
+ _markHand(this, d);
1446
1514
  this._requireDeclared(d, 'placeDeclaredAt');
1447
1515
  if (!Array.isArray(point) || (point.length !== 2 && point.length !== 3) ||
1448
1516
  !point.every(function (v) { return typeof v === 'number' && isFinite(v); })) {
@@ -1463,9 +1531,10 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1463
1531
  zHeld = Math.abs(k[2]) > FrqtlApp.Z_HOLD_EPS;
1464
1532
  k = [k[0], k[1], 0];
1465
1533
  }
1466
- this.updateObjectProperty(d, 'rect', L.rectAtCenter(d.rect, k));
1534
+ // A hand places on the pixel lattice (LoDrawnCenter: the position snaps, the size is kept).
1535
+ this.updateObjectProperty(d, 'rect', L.snapRectPosition(L.rectAtCenter(d.rect, k)));
1467
1536
  } else {
1468
- this.updateObjectProperty(d, 'center', k);
1537
+ this.updateObjectProperty(d, 'center', LoDrawnCenter.snapCenter(k));
1469
1538
  }
1470
1539
  return { center: this.centerOf(d), zHeld: zHeld };
1471
1540
  };
@@ -1556,6 +1625,9 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1556
1625
  /** Beyond this |z| (world px) a carried centre is not expressible by a z-LESS rect (it sits
1557
1626
  * at z-centre 0 — lo-drawn-center): the object stays on the z = 0 plane and says Z HELD. */
1558
1627
  FrqtlApp.Z_HOLD_EPS = 1e-6;
1628
+ /** The one wording of "a parts entry is never renamed by a hand" (updateObjectProperty, RecipeContext). */
1629
+ FrqtlApp.RENAME_REFUSAL = 'the key is the name — a rename is a new key in `parts` (with every reference to it, ' +
1630
+ 'in code) or a fork of the sketch; a hand never writes a `name` into an entry.';
1559
1631
 
1560
1632
  var AIM_OFF = 'off', AIM_ROAMING = 'roaming', AIM_READY = 'grab-ready', AIM_GRABBED = 'grabbed';
1561
1633
 
@@ -2182,9 +2254,10 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
2182
2254
  /**
2183
2255
  * retainSceneReplay(fn) — the ONE retainer of "how this runner's scene was built", so
2184
2256
  * `restart()` can replay it. Whichever path built the scene sets it: `setup(factory, opts)`
2185
- * retains its factory replay; `RecipeContext.build(sketch)` retains `exp.build(sketch)`. A later
2186
- * build replaces the earlier retention (the runner holds ONE scene).
2187
- * @param {Function} fn — replays the scene; its return value is restart()'s.
2257
+ * retains its factory replay; `RecipeContext.build(sketch)` retains its rebuild (a sketch built from
2258
+ * linked text re-links the facade's CURRENT text — what the writer last wrote — and builds that). A
2259
+ * later build replaces the earlier retention (the runner holds ONE scene).
2260
+ * @param {Function} fn — replays the scene; its return value (or the value its promise resolves) is restart()'s.
2188
2261
  */
2189
2262
  FrqtlApp.prototype.retainSceneReplay = function (fn) {
2190
2263
  if (typeof fn !== 'function') {
@@ -2195,25 +2268,24 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
2195
2268
  };
2196
2269
 
2197
2270
  /**
2198
- * restart() — the PAGE-LEVEL restart primitive (presentation pass Item 2,
2199
- * E-P2b): stop → replay the RETAINED scene (`retainSceneReplay` — a setup
2200
- * factory's world + part steps with its ORIGINAL opts, or a sketch's `exp.build(sketch)`
2201
- * on the same runner and canvas) → start. The scene returns to tick 0 IN-PAGE: no reload,
2202
- * so the camera orbit, render options and any host wiring survive. The declared `restart`
2203
- * control (controlAction.js) routes here for every host.
2271
+ * restart() → Promise — the PAGE-LEVEL restart primitive: stop → replay the RETAINED scene
2272
+ * (`retainSceneReplay` — a setup factory's world + part steps with its ORIGINAL opts, a sketch's
2273
+ * rebuild on the same runner and canvas, or — for a sketch built from linked text — a re-link of the
2274
+ * facade's current text, then its build) → start. The scene returns to tick 0 IN-PAGE: no reload, so
2275
+ * the camera orbit, render options and any host wiring survive. The declared `restart` control
2276
+ * (controlAction.js) routes here for every host.
2204
2277
  *
2205
- * Teardown runs EXACTLY ONCE — the replay owns it (`setup()` calls the previous
2206
- * setupResult's `dispose()`; a sketch build tears the previous scene down first —
2207
- * RecipeContext.build), so this method must NOT dispose again (double teardown was the
2208
- * stacking-regression this seam was built to avoid).
2278
+ * ALWAYS a Promise. A replay that returns a plain value (a factory, a function-built sketch) has run
2279
+ * and the loop has restarted before restart() returns; its promise resolves at once. A replay that
2280
+ * returns a promise (a re-link) resolves after the rebuild and the start. Every caller awaits it.
2209
2281
  *
2210
- * DISTINCT BY DESIGN from STUDIO's codegen Restart (AuthorView), which rebuilds
2211
- * a persisted DESIGN and reloads the page: that one is STUDIO chrome and stays
2212
- * where it is. This one replays the already-loaded module.
2282
+ * Teardown runs EXACTLY ONCE — the replay owns it (`setup()` calls the previous setupResult's
2283
+ * `dispose()`; a sketch build tears the previous scene down first — RecipeContext.build), so this
2284
+ * method must NOT dispose again.
2213
2285
  *
2214
- * @returns {*} the replay's result (a setup result, or the sketch's facade).
2215
- * @throws {Error} FAIL-LOUD when nothing built a scene on this runner (there is nothing
2216
- * to replay) — never a silent no-op.
2286
+ * @returns {Promise<*>} the replay's result (a setup result, or the sketch's facade).
2287
+ * @throws {Error} FAIL-LOUD when nothing built a scene on this runner (there is nothing to replay) —
2288
+ * never a silent no-op; a failed replay REJECTS.
2217
2289
  */
2218
2290
  FrqtlApp.prototype.restart = function () {
2219
2291
  if (typeof this._sceneReplay !== 'function') {
@@ -2222,12 +2294,19 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
2222
2294
  '(FAIL-LOUD — a restart with nothing to replay would silently do nothing.)');
2223
2295
  }
2224
2296
  this.stop();
2225
- var result = this._sceneReplay();
2226
- this._lastTickData = null;
2227
- // Always resume: "restart" means run the scene again from tick 0 — a restart
2228
- // that left the loop paused would read as a dead button.
2229
- this.start();
2230
- return result;
2297
+ var self = this;
2298
+ function resume(result) {
2299
+ self._lastTickData = null;
2300
+ // Always resume: "restart" means run the scene again from tick 0 — a restart
2301
+ // that left the loop paused would read as a dead button.
2302
+ self.start();
2303
+ return result;
2304
+ }
2305
+ var result;
2306
+ try { result = this._sceneReplay(); }
2307
+ catch (err) { return Promise.reject(err); }
2308
+ if (result && typeof result.then === 'function') { return result.then(resume); }
2309
+ return Promise.resolve(resume(result));
2231
2310
  };
2232
2311
 
2233
2312
  // ── Renderer resolution ────────────────────────────────────────────────
@@ -95,8 +95,7 @@ import { V1_REPLACEMENTS } from './v1-replacements.js';
95
95
  // `preset` is expanded by the OBJECT constructor (Wall.expandWallPreset), not by
96
96
  // a builder — it reaches the constructor through the forwarded props bag.
97
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).
98
+ // A PointSource's construction-only opts (its builder reads them; no descriptor field).
100
99
  '_wavelength', '_optColor', '_emergentPhysics'
101
100
  ];
102
101
 
@@ -187,7 +186,7 @@ import { V1_REPLACEMENTS } from './v1-replacements.js';
187
186
  * returns (never one of its members) is recorded in `ctx.runner.declared` with the census LOs
188
187
  * the build added as its members and the host tick-functions the build pushed
189
188
  * (`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
189
+ * declaring call (`exp.addObject`, `exp.declare`, a palette drop) produced is what STUDIO
191
190
  * holds; a part's composition is never declared.
192
191
  *
193
192
  * FAIL-LOUD: an unknown kind THROWS naming the registered kinds and, when
@@ -264,11 +263,11 @@ import { V1_REPLACEMENTS } from './v1-replacements.js';
264
263
  'scene. (FAIL-LOUD.)');
265
264
  }
266
265
  // ... and the host tick-functions it pushed are the declaration's too (an emitter's emit
267
- // fn, an absorb radiator): behaviour the declaration owns, so codegen may write it down.
266
+ // fn, an absorb radiator): behaviour the declaration owns.
268
267
  var tfsAfter = (runner.universe && Array.isArray(runner.universe.experimentTickFunctions))
269
268
  ? runner.universe.experimentTickFunctions : [];
270
269
  var ticks = tfsAfter.filter(function (fn) { return !ticksBefore.has(fn); });
271
- runner.declare(handle, members, ticks);
270
+ runner.declare(handle, members, ticks, { kind: kind });
272
271
  return handle;
273
272
  },
274
273