@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p4.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 +8 -4
- package/dist/dist-manifest.json +70 -62
- package/dist/eyes/index.js +5 -2
- 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 +136 -30
- package/dist/frqtl/objects/ObjectFactory.js +4 -5
- package/dist/frqtl/objects/RecipeContext.js +617 -9
- package/dist/frqtl/objects/instruments/hosted-eyes.js +268 -57
- package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
- package/dist/frqtl/objects/record/Record.js +326 -0
- package/dist/frqtl/objects/record/open-record.js +22 -0
- 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/FrqtlVision.js +21 -6
- 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/index.js +112 -96
- package/dist/shim/bundle/indexHtml.js +66 -39
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/index.js +1 -1
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/store.js +2 -3
- package/dist/shim/page-params/index.js +2 -2
- package/dist/shim/project/index.js +20 -5
- package/dist/shim/project/link.js +182 -37
- package/dist/shim/project/manifest.js +151 -0
- package/dist/shim/project/parts-module.js +67 -0
- package/dist/shim/project/parts-writer.js +457 -0
- package/dist/shim/project/record-format.js +345 -0
- package/dist/shim/project/specifiers.js +41 -13
- package/dist/shim/registry/index.js +1 -1
- package/dist/shim/registry/scene-source.js +44 -30
- package/dist/shim/registry/walkthrough.js +13 -12
- package/dist/shim/render-state/apply.js +11 -14
- package/dist/shim/storage/codec-chains.js +88 -0
- package/dist/shim/storage/folders.js +205 -0
- package/dist/shim/storage/index.js +64 -8
- package/dist/shim/storage/make-namespace-store.js +5 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/dist/shim/bundle/frqnb.js +0 -112
- package/dist/shim/bundle/manifest.js +0 -95
- package/dist/shim/storage/modules.js +0 -249
- package/dist/shim/storage/projects.js +0 -150
|
@@ -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,13 @@ 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';
|
|
78
|
+
import { MANIFEST_FILE } from '../../shim/project/manifest.js';
|
|
79
|
+
import { Record, openRecordOf } from './record/Record.js';
|
|
46
80
|
|
|
47
81
|
/** The world-init contract (objects/setup/_worldInit.js). */
|
|
48
82
|
function _worldInit() {
|
|
@@ -86,6 +120,13 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
86
120
|
this._world = null; // set by the co-init call; reset by build()
|
|
87
121
|
this._built = false;
|
|
88
122
|
this._buildFn = null;
|
|
123
|
+
this._project = null; // THE held project text: { files: {path: text}, entry, mint, bare? } (build(linked))
|
|
124
|
+
this._urls = null; // the minted URL of each project file, from the last link
|
|
125
|
+
this._parts = new Map(); // declared object → { key, baseline } — the objects in `parts`
|
|
126
|
+
this._code = new Map(); // declared object → baseline — the code declarations at build end
|
|
127
|
+
this._declaredOnce = false;
|
|
128
|
+
this._record = null; // the open Record (exp.startRecord), or null
|
|
129
|
+
this._reauthored = null; // the Record the last re-authoring closed (startRecord checks its close was heard)
|
|
89
130
|
}
|
|
90
131
|
|
|
91
132
|
function _requireWorld(self) {
|
|
@@ -148,6 +189,8 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
148
189
|
self._runner = new FrqtlApp(ropts);
|
|
149
190
|
if (!self._opts.deferCanvas) { self._runner.createCanvas(); }
|
|
150
191
|
}
|
|
192
|
+
// The census maps a declared kind's module against the project files this facade linked (FrqtlApp.declare).
|
|
193
|
+
self._runner._projectUrls = self._urls;
|
|
151
194
|
var u = self._runner.universe;
|
|
152
195
|
var args = W.initArgs(cfg);
|
|
153
196
|
if (renderer === 'three') {
|
|
@@ -156,6 +199,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
156
199
|
}
|
|
157
200
|
// A (re-)inited world has no declared objects; the recipe re-declares what it builds.
|
|
158
201
|
self._runner.clearDeclared();
|
|
202
|
+
self._runner.beginWorld(); // a (re-)inited world has not stepped
|
|
159
203
|
W.applyPreInit(cfg, u); // pre-init world keys (iframeIntervalSeconds) — the engine reads them at init
|
|
160
204
|
u.init.apply(u, args);
|
|
161
205
|
self._world = Object.freeze({ renderer: renderer, w: w, h: h, d: renderer === 'three' ? d : null,
|
|
@@ -190,6 +234,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
190
234
|
|
|
191
235
|
// ── parts, tick-fns, render ──────────────────────────────────────────────────────
|
|
192
236
|
|
|
237
|
+
/** exp.addObject(kind, config) — a kind by name (a host's JSON) or by its class. */
|
|
193
238
|
RecipeContext.prototype.addObject = function (kind, config) {
|
|
194
239
|
_requireWorld(this);
|
|
195
240
|
var x = (config && config.x != null) ? config.x : 0;
|
|
@@ -198,6 +243,487 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
198
243
|
return ObjectFactory.create(kind, config, ctx);
|
|
199
244
|
};
|
|
200
245
|
|
|
246
|
+
// ── the parts block ───────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
var IMPORT_FORM = 'import { <Kind> } from \'@descix/frqtl-web/parts\'';
|
|
249
|
+
|
|
250
|
+
/** A deep copy of arrays and plain objects (the module-level literal is never shared with a part). */
|
|
251
|
+
function _plain(v) {
|
|
252
|
+
if (v === null || typeof v !== 'object') { return v; }
|
|
253
|
+
if (Array.isArray(v)) { return v.map(_plain); }
|
|
254
|
+
var proto = Object.getPrototypeOf(v);
|
|
255
|
+
if (proto !== Object.prototype && proto !== null) { return v; }
|
|
256
|
+
var out = {};
|
|
257
|
+
Object.keys(v).forEach(function (k) { out[k] = _plain(v[k]); });
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* exp.declare(parts) — seed the declared parts of `export const parts = { name: [Kind, props], … }`, in key
|
|
263
|
+
* order: `exp.addObject(Kind, { name: key, ...props })` per own key. Packaging, zero physics. Each object
|
|
264
|
+
* declared here is an `inParts` object of the census, and its capture right after construction is the
|
|
265
|
+
* writer's baseline (toSketch diffs against it).
|
|
266
|
+
* RETURNS the build-time map `Object.freeze({ <key>: handle, … })`, in key order — each value the object
|
|
267
|
+
* exp.addObject returned (the same identity as its runner.declared row), so later scene code reads a part
|
|
268
|
+
* as `const p = exp.declare(parts); … p.screen`. A hand add or remove after the build does not update it.
|
|
269
|
+
* REFUSES (throws): a string Kind (name the import form), a Kind that is not a registered class, a
|
|
270
|
+
* `props.name` (the key IS the name), a second declare in one build, an entry not written [Kind, props].
|
|
271
|
+
*/
|
|
272
|
+
RecipeContext.prototype.declare = function (parts) {
|
|
273
|
+
_requireWorld(this);
|
|
274
|
+
if (this._declaredOnce) {
|
|
275
|
+
throw new Error('[RecipeContext.declare] exp.declare(parts) runs ONCE per build — the scene\'s one `export const ' +
|
|
276
|
+
PARTS_EXPORT + '` block is its declared parts. (FAIL-LOUD.)');
|
|
277
|
+
}
|
|
278
|
+
if (!parts || typeof parts !== 'object' || Array.isArray(parts)) {
|
|
279
|
+
throw new TypeError('[RecipeContext.declare] exp.declare(parts): parts is the sketch\'s `export const ' + PARTS_EXPORT +
|
|
280
|
+
' = { name: [Kind, { …props }] }` object. (FAIL-LOUD.)');
|
|
281
|
+
}
|
|
282
|
+
this._declaredOnce = true;
|
|
283
|
+
var self = this;
|
|
284
|
+
var out = {};
|
|
285
|
+
Object.keys(parts).forEach(function (key) {
|
|
286
|
+
var entry = parts[key];
|
|
287
|
+
if (!Array.isArray(entry) || entry.length !== 2 || !entry[1] || typeof entry[1] !== 'object' || Array.isArray(entry[1])) {
|
|
288
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ' is not [Kind, { …props }]. (FAIL-LOUD.)');
|
|
289
|
+
}
|
|
290
|
+
var Kind = entry[0], props = entry[1];
|
|
291
|
+
if (typeof Kind === 'string') {
|
|
292
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ' names its kind by the string "' + Kind + '" — in ' +
|
|
293
|
+
'`parts` a kind is the imported class: ' + IMPORT_FORM.replace('<Kind>', Kind) + '. (exp.addObject(name, ' +
|
|
294
|
+
'props) still takes a string.) (FAIL-LOUD.)');
|
|
295
|
+
}
|
|
296
|
+
if (typeof Kind !== 'function' || !kindOf(Kind)) {
|
|
297
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ': ' + (typeof Kind === 'function'
|
|
298
|
+
? 'the class ' + (Kind.name || '(anonymous)') + ' is not a registered kind in this realm'
|
|
299
|
+
: 'the kind is ' + (Kind === null ? 'null' : typeof Kind) + ', not a registered class') +
|
|
300
|
+
' — ' + IMPORT_FORM + ', or register a forked part\'s renamed class (@descix/frqtl-web/kinds). (FAIL-LOUD.)');
|
|
301
|
+
}
|
|
302
|
+
if (Object.prototype.hasOwnProperty.call(props, 'name')) {
|
|
303
|
+
throw new TypeError('[RecipeContext.declare] parts.' + key + ' carries name ' + JSON.stringify(props.name) +
|
|
304
|
+
' — in `parts` the KEY is the name ("' + key + '"); delete the name prop. (FAIL-LOUD.)');
|
|
305
|
+
}
|
|
306
|
+
var obj = self.addObject(Kind, Object.assign({ name: key }, _plain(props)));
|
|
307
|
+
self._runner._adoptIntoParts(obj);
|
|
308
|
+
self._parts.set(obj, { key: key, baseline: _capture(self, obj), kind: self._runner.kindOf(obj) });
|
|
309
|
+
out[key] = obj;
|
|
310
|
+
});
|
|
311
|
+
return Object.freeze(out);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
function _capture(self, obj) {
|
|
315
|
+
return ObjectSchema.captureProps(obj, self._runner.kindOf(obj));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The fields a hand or setDeclared may write: the descriptor's, minus write-only adapters and readonly reads. */
|
|
319
|
+
function _writable(kind) {
|
|
320
|
+
var d = ObjectSchema.get(kind);
|
|
321
|
+
var out = {};
|
|
322
|
+
((d && d.fields) || []).forEach(function (f) {
|
|
323
|
+
if (f.writeOnly !== true && f.type !== 'readonly') { out[f.name] = f; }
|
|
324
|
+
});
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The writable keys whose live value differs from the baseline (JSON-equal = untouched). */
|
|
329
|
+
function _touched(kind, baseline, live) {
|
|
330
|
+
var w = _writable(kind);
|
|
331
|
+
var out = {};
|
|
332
|
+
var lost = [];
|
|
333
|
+
Object.keys(w).forEach(function (k) {
|
|
334
|
+
var a = baseline[k], b = live[k];
|
|
335
|
+
if (JSON.stringify(a) === JSON.stringify(b)) { return; }
|
|
336
|
+
if (b === undefined) { lost.push(k); return; }
|
|
337
|
+
out[k] = b;
|
|
338
|
+
});
|
|
339
|
+
return { props: out, lost: lost };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
var RENAME = FrqtlApp.RENAME_REFUSAL;
|
|
343
|
+
|
|
344
|
+
var _JSON = function (v) { return JSON.stringify(v); };
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Spell a touched rect in the entry's OWN form (architect z addendum). A 6-tuple rect of a kind whose descriptor
|
|
348
|
+
* derives reads from it (ObjectSchema.derivedReads: Wall `z`, `depth`) is written as the 4-tuple plus each derived
|
|
349
|
+
* key whose value changed, when the entry spells any of those keys as plain properties (entryKeys); otherwise the
|
|
350
|
+
* 6-tuple stands. A 4-tuple equal to the baseline's is not written (no literalizing an untouched expression).
|
|
351
|
+
* Mutates and returns `props`.
|
|
352
|
+
*/
|
|
353
|
+
function _spell(text, name, kind, props, baseline) {
|
|
354
|
+
var derived = ObjectSchema.derivedReads(kind);
|
|
355
|
+
if (!derived.length) { return props; }
|
|
356
|
+
var spelled = entryKeys(text, name);
|
|
357
|
+
var R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
|
|
358
|
+
Object.keys(props).forEach(function (f) {
|
|
359
|
+
var v = props[f];
|
|
360
|
+
var rows = derived.filter(function (d) { return d.field === f && d.tuple === 6 && spelled.indexOf(d.name) !== -1; });
|
|
361
|
+
if (!rows.length || !Array.isArray(v) || v.length !== 6) { return; }
|
|
362
|
+
var b = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f] : null;
|
|
363
|
+
var four = R4.map(function (k) { return v[R6.indexOf(k)]; });
|
|
364
|
+
if (b && _JSON(four) === _JSON(R4.map(function (k) { return b[R6.indexOf(k)]; }))) { delete props[f]; }
|
|
365
|
+
else { props[f] = four; }
|
|
366
|
+
rows.forEach(function (d) {
|
|
367
|
+
var nv = v[R6.indexOf(d.key)];
|
|
368
|
+
if (!b || nv !== b[R6.indexOf(d.key)]) { props[d.name] = nv; }
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
return props;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Spell a touched `center` in the entry's OWN arity: two numbers when the entry
|
|
376
|
+
* spells two (a literal, or an array expression — entryLiteral's `length`) and the live z is 0; otherwise the
|
|
377
|
+
* live [x, y, z] (an entry spelled three, a 3D hand off z 0, an entry whose center is not an array). The
|
|
378
|
+
* read-back (_verifyWritten) guards every case. Mutates and returns `props`.
|
|
379
|
+
*/
|
|
380
|
+
function _spellCenter(text, name, props) {
|
|
381
|
+
var v = props.center;
|
|
382
|
+
if (!Array.isArray(v) || v.length !== 3 || v[2] !== 0) { return props; }
|
|
383
|
+
if (entryKeys(text, name).indexOf('center') === -1) { return props; }
|
|
384
|
+
var r = entryLiteral(text, name, 'center');
|
|
385
|
+
var n = r.literal ? (Array.isArray(r.value) ? r.value.length : null) : r.length;
|
|
386
|
+
if (n === 2) { props.center = [v[0], v[1]]; }
|
|
387
|
+
return props;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The post-patch check: what the patched text now declares for entry `name`, read back (entryLiteral) for every
|
|
392
|
+
* written key and folded with the baseline for every unwritten one, must equal the live capture — `rect` in its
|
|
393
|
+
* capture form, through the descriptor's derivedFrom rows. Each mismatch is a refusal naming the key.
|
|
394
|
+
*/
|
|
395
|
+
function _verifyWritten(text, name, kind, written, baseline, live) {
|
|
396
|
+
var refused = [];
|
|
397
|
+
var R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
|
|
398
|
+
var derived = ObjectSchema.derivedReads(kind);
|
|
399
|
+
var decl = {};
|
|
400
|
+
Object.keys(written).forEach(function (k) {
|
|
401
|
+
var r = entryLiteral(text, name, k);
|
|
402
|
+
decl[k] = r.literal ? r.value : undefined;
|
|
403
|
+
// A two-number center declares z 0 (the reading placeDeclaredAt gives an omitted z).
|
|
404
|
+
if (k === 'center' && Array.isArray(decl[k]) && decl[k].length === 2) { decl[k] = decl[k].concat([0]); }
|
|
405
|
+
});
|
|
406
|
+
function refuse(k, declared, liveV) {
|
|
407
|
+
refused.push({ op: 'set', name: name, key: k, reason: 'the written text declares ' + _JSON(declared) + ' but the live object is ' +
|
|
408
|
+
_JSON(liveV) + ' — the hand is not in the text.' });
|
|
409
|
+
}
|
|
410
|
+
var asDerived = {};
|
|
411
|
+
derived.forEach(function (d) { asDerived[d.name] = d; });
|
|
412
|
+
Object.keys(written).forEach(function (k) {
|
|
413
|
+
if (asDerived[k]) { return; }
|
|
414
|
+
var lv = live[k];
|
|
415
|
+
if (Array.isArray(lv) && lv.length === 6 && derived.some(function (d) { return d.field === k; })) { return; }
|
|
416
|
+
if (_JSON(decl[k]) !== _JSON(lv)) { refuse(k, decl[k], lv); }
|
|
417
|
+
});
|
|
418
|
+
var fields = {};
|
|
419
|
+
derived.forEach(function (d) { fields[d.field] = true; });
|
|
420
|
+
Object.keys(fields).forEach(function (f) {
|
|
421
|
+
var lv = live[f];
|
|
422
|
+
if (!Array.isArray(lv) || lv.length !== 6) { return; }
|
|
423
|
+
var touched = written[f] !== undefined || derived.some(function (d) { return d.field === f && written[d.name] !== undefined; });
|
|
424
|
+
if (!touched) { return; }
|
|
425
|
+
var six = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f].slice() : null;
|
|
426
|
+
var d0 = decl[f];
|
|
427
|
+
if (Array.isArray(d0) && d0.length === 6) { six = d0.slice(); }
|
|
428
|
+
else if (Array.isArray(d0) && d0.length === 4 && six) { R4.forEach(function (k, i) { six[R6.indexOf(k)] = d0[i]; }); }
|
|
429
|
+
derived.forEach(function (d) {
|
|
430
|
+
if (d.field === f && six && decl[d.name] !== undefined) { six[R6.indexOf(d.key)] = decl[d.name]; }
|
|
431
|
+
});
|
|
432
|
+
if (_JSON(six) !== _JSON(lv)) { refuse(f, six, lv); }
|
|
433
|
+
});
|
|
434
|
+
return refused;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
var OUTSIDE_PARTS = 'this sketch declares its parts outside `parts`; STUDIO can run it but cannot write hand edits ' +
|
|
438
|
+
'into it — declare them in `export const parts` to make it editable.';
|
|
439
|
+
|
|
440
|
+
function _requireHeld(self, where) {
|
|
441
|
+
if (!self._project) {
|
|
442
|
+
throw new Error('[RecipeContext.' + where + '] this scene was not built from linked text — the writer writes the ' +
|
|
443
|
+
'text a facade linked: exp.build(await linkProject({ files, entry, mint })) (lib/shim/project/link.js). (FAIL-LOUD.)');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* exp.toSketch() → Promise<{ files, changes, refused }> — write the hands into the sketch's `parts` literal.
|
|
449
|
+
* 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
|
|
450
|
+
* placement. For each declared object a hand wrote since the last call (the runner's hand mark: a device
|
|
451
|
+
* door — updateObjectProperty, placeDeclaredAt — wrote it; a part the scene's own code moves is the run,
|
|
452
|
+
* not a hand), it diffs the `inParts` object's live capture against its baseline (touched keys only →
|
|
453
|
+
* `set`), writes each declared object added since the build (`add`, as the last key, plus its import line)
|
|
454
|
+
* and each removed `inParts` object (`remove`), through lib/shim/project/parts-writer.js patchParts on the
|
|
455
|
+
* HELD text. A code-declared object a hand touched (or removed) is refused by name. On return the patched
|
|
456
|
+
* text IS the held text and every baseline is the live capture, so a second call with no hand between
|
|
457
|
+
* returns byte-identical files and no changes.
|
|
458
|
+
* files { path: text } — the whole project as now held
|
|
459
|
+
* changes patchParts' changes (a `literalized` set names the expression it replaced)
|
|
460
|
+
* refused every hand not written, { op, name, key?, reason }
|
|
461
|
+
*/
|
|
462
|
+
RecipeContext.prototype.toSketch = async function (opts) {
|
|
463
|
+
_requireWorld(this);
|
|
464
|
+
_requireHeld(this, 'toSketch');
|
|
465
|
+
var by = this._record ? Record.requireBy(opts && opts.by, 'RecipeContext.toSketch') : null;
|
|
466
|
+
var self = this;
|
|
467
|
+
var runner = this._runner;
|
|
468
|
+
var declared = runner.declared;
|
|
469
|
+
var edits = [];
|
|
470
|
+
var refused = [];
|
|
471
|
+
var adopted = [];
|
|
472
|
+
var hands = runner._takeHandTouched();
|
|
473
|
+
this._parts.forEach(function (row, obj) {
|
|
474
|
+
if (declared.indexOf(obj) === -1) { edits.push({ op: 'remove', name: row.key, kind: row.kind, baseline: row.baseline }); return; }
|
|
475
|
+
if (!hands.has(obj)) { return; }
|
|
476
|
+
var kind = runner.kindOf(obj);
|
|
477
|
+
var t = _touched(kind, row.baseline, _capture(self, obj));
|
|
478
|
+
if (Object.prototype.hasOwnProperty.call(t.props, 'name')) {
|
|
479
|
+
refused.push({ op: 'set', name: row.key, key: 'name', reason: RENAME });
|
|
480
|
+
delete t.props.name;
|
|
481
|
+
}
|
|
482
|
+
t.lost.forEach(function (k) {
|
|
483
|
+
refused.push({ op: 'set', name: row.key, key: k, reason: 'the live object no longer reports "' + k + '" — there is no value to write.' });
|
|
484
|
+
});
|
|
485
|
+
if (Object.keys(t.props).length) {
|
|
486
|
+
var raw = _plain(t.props); // the capture form, before the entry's spelling (the Record's row values)
|
|
487
|
+
_spell(self._project.files[self._project.entry], row.key, kind, t.props, row.baseline);
|
|
488
|
+
_spellCenter(self._project.files[self._project.entry], row.key, t.props);
|
|
489
|
+
if (Object.keys(t.props).length) { edits.push({ op: 'set', name: row.key, props: t.props, obj: obj, kind: kind, baseline: row.baseline, raw: raw }); }
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
this._code.forEach(function (baseline, obj) {
|
|
493
|
+
var name = obj.name || runner.kindOf(obj);
|
|
494
|
+
if (declared.indexOf(obj) === -1) {
|
|
495
|
+
refused.push({ op: 'remove', name: name, reason: '"' + name + '" is declared in code: ' + OUTSIDE_PARTS });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!hands.has(obj)) { return; }
|
|
499
|
+
var t = _touched(runner.kindOf(obj), baseline, _capture(self, obj));
|
|
500
|
+
if (Object.keys(t.props).length || t.lost.length) {
|
|
501
|
+
refused.push({ op: 'set', name: name, reason: '"' + name + '" is declared in code: ' + OUTSIDE_PARTS });
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
declared.forEach(function (obj) {
|
|
505
|
+
if (self._parts.has(obj) || self._code.has(obj)) { return; }
|
|
506
|
+
var kind = runner.kindOf(obj);
|
|
507
|
+
var row = kindOf(kind);
|
|
508
|
+
var props = _capture(self, obj);
|
|
509
|
+
var w = _writable(kind);
|
|
510
|
+
Object.keys(props).forEach(function (k) { if (!w[k] || k === 'name') { delete props[k]; } });
|
|
511
|
+
edits.push({ op: 'add', name: obj.name || kind, Kind: kind, module: row ? moduleOfKind(row.from, self._urls) : null, props: props });
|
|
512
|
+
adopted.push(obj);
|
|
513
|
+
});
|
|
514
|
+
var entry = this._project.entry;
|
|
515
|
+
var r = patchParts(this._project.files[entry], edits.map(function (e) {
|
|
516
|
+
if (e.op === 'set') { return { op: 'set', name: e.name, props: e.props }; }
|
|
517
|
+
if (e.op === 'remove') { return { op: 'remove', name: e.name }; }
|
|
518
|
+
return { op: 'add', name: e.name, Kind: e.Kind, module: e.module, props: e.props };
|
|
519
|
+
}), { entry: entry });
|
|
520
|
+
// Never decide it succeeded without asking: read the patched text back against each moved object.
|
|
521
|
+
edits.forEach(function (e) {
|
|
522
|
+
if (e.op !== 'set') { return; }
|
|
523
|
+
if (r.refused.some(function (x) { return x.op === 'set' && x.name === e.name; })) { return; }
|
|
524
|
+
refused.push.apply(refused, _verifyWritten(r.text, e.name, e.kind, e.props, e.baseline, _capture(self, e.obj)));
|
|
525
|
+
});
|
|
526
|
+
var refusedAdd = {};
|
|
527
|
+
r.refused.forEach(function (x) { if (x.op === 'add') { refusedAdd[x.name] = true; } });
|
|
528
|
+
var files = Object.assign({}, this._project.files);
|
|
529
|
+
files[entry] = r.text;
|
|
530
|
+
this._project = Object.assign({}, this._project, { files: files });
|
|
531
|
+
// Adopt: the written text is held; baselines follow the live objects.
|
|
532
|
+
var removedRefused = {};
|
|
533
|
+
r.refused.forEach(function (x) { if (x.op === 'remove') { removedRefused[x.name] = true; } });
|
|
534
|
+
this._parts.forEach(function (row, obj) {
|
|
535
|
+
if (declared.indexOf(obj) === -1) { if (!removedRefused[row.key]) { self._parts.delete(obj); } return; }
|
|
536
|
+
row.baseline = _capture(self, obj);
|
|
537
|
+
});
|
|
538
|
+
adopted.forEach(function (obj) {
|
|
539
|
+
var key = obj.name || runner.kindOf(obj);
|
|
540
|
+
if (refusedAdd[key]) { return; }
|
|
541
|
+
runner._adoptIntoParts(obj);
|
|
542
|
+
self._parts.set(obj, { key: key, baseline: _capture(self, obj), kind: runner.kindOf(obj) });
|
|
543
|
+
});
|
|
544
|
+
var allRefused = refused.concat(r.refused);
|
|
545
|
+
if (this._record) { _recordRows(this, edits, allRefused, by); }
|
|
546
|
+
return { files: Object.assign({}, files), changes: r.changes, refused: allRefused };
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* The Record's rows for one toSketch, from the writer's own edits as WRITTEN (a refused edit, or a refused key
|
|
551
|
+
* of one, makes no row), in edit order (R3). A `set` whose touched keys include the part's position authority
|
|
552
|
+
* (runner._placementAuthority: `center`, or `rect` with its derivedFrom reads) is a `move` with that ONE key in
|
|
553
|
+
* its capture form; its other touched keys are one `setDeclared` row. `add` → [kind, props as written];
|
|
554
|
+
* `remove` → [kind, the baseline's writable props].
|
|
555
|
+
*/
|
|
556
|
+
function _recordRows(self, edits, refused, by) {
|
|
557
|
+
var runner = self._runner;
|
|
558
|
+
var rec = self._record;
|
|
559
|
+
function refusedKeys(name, op) {
|
|
560
|
+
var keys = {};
|
|
561
|
+
var all = false;
|
|
562
|
+
refused.forEach(function (x) {
|
|
563
|
+
if (x.name !== name || x.op !== op) { return; }
|
|
564
|
+
if (x.key === undefined) { all = true; } else { keys[x.key] = true; }
|
|
565
|
+
});
|
|
566
|
+
return all ? null : keys;
|
|
567
|
+
}
|
|
568
|
+
function writableProps(kind, bag) {
|
|
569
|
+
var w = _writable(kind);
|
|
570
|
+
var out = {};
|
|
571
|
+
Object.keys(bag).forEach(function (k) { if (w[k] && k !== 'name') { out[k] = _plain(bag[k]); } });
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
edits.forEach(function (e) {
|
|
575
|
+
if (e.op === 'add') {
|
|
576
|
+
if (refusedKeys(e.name, 'add') === null) { return; }
|
|
577
|
+
rec._row({ name: e.name, op: 'add', by: by, from: null, to: [e.Kind, _plain(e.props)] });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (e.op === 'remove') {
|
|
581
|
+
if (refusedKeys(e.name, 'remove') === null) { return; }
|
|
582
|
+
rec._row({ name: e.name, op: 'remove', by: by, from: [e.kind, writableProps(e.kind, e.baseline)], to: null });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
var bad = refusedKeys(e.name, 'set');
|
|
586
|
+
if (bad === null) { return; }
|
|
587
|
+
var raw = e.raw;
|
|
588
|
+
var auth = (raw.center !== undefined || raw.rect !== undefined) ? runner._placementAuthority(e.obj) : null;
|
|
589
|
+
var authReads = {};
|
|
590
|
+
if (auth) {
|
|
591
|
+
authReads[auth] = true;
|
|
592
|
+
ObjectSchema.derivedReads(e.kind).forEach(function (d) { if (d.field === auth) { authReads[d.name] = true; } });
|
|
593
|
+
}
|
|
594
|
+
var moveRefused = Object.keys(bad).some(function (k) { return authReads[k]; });
|
|
595
|
+
if (auth && raw[auth] !== undefined && !moveRefused) {
|
|
596
|
+
var from = {}, to = {};
|
|
597
|
+
from[auth] = _plain(e.baseline[auth]);
|
|
598
|
+
to[auth] = _plain(raw[auth]);
|
|
599
|
+
rec._row({ name: e.name, op: 'move', by: by, from: from, to: to });
|
|
600
|
+
}
|
|
601
|
+
var pf = {}, pt = {}, any = false;
|
|
602
|
+
Object.keys(raw).forEach(function (k) {
|
|
603
|
+
if (k === auth || bad[k]) { return; }
|
|
604
|
+
pf[k] = e.baseline[k] === undefined ? null : _plain(e.baseline[k]);
|
|
605
|
+
pt[k] = _plain(raw[k]);
|
|
606
|
+
any = true;
|
|
607
|
+
});
|
|
608
|
+
if (any) { rec._row({ name: e.name, op: 'setDeclared', by: by, from: pf, to: pt }); }
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** The names of declared objects a hand marked whose live capture differs from their baseline (not yet written). */
|
|
613
|
+
function _unwrittenHands(self) {
|
|
614
|
+
var runner = self._runner;
|
|
615
|
+
var marks = runner._handTouched;
|
|
616
|
+
if (!marks || !marks.size) { return []; }
|
|
617
|
+
var out = [];
|
|
618
|
+
marks.forEach(function (obj) {
|
|
619
|
+
if (runner.declared.indexOf(obj) === -1) { return; }
|
|
620
|
+
var row = self._parts.get(obj);
|
|
621
|
+
var baseline = row ? row.baseline : self._code.get(obj);
|
|
622
|
+
if (!baseline) { return; }
|
|
623
|
+
var t = _touched(runner.kindOf(obj), baseline, _capture(self, obj));
|
|
624
|
+
if (Object.keys(t.props).length || t.lost.length) { out.push(row ? row.key : (obj.name || runner.kindOf(obj))); }
|
|
625
|
+
});
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** The camera pose the runner's camera declares (poseFields), or null with no camera. */
|
|
630
|
+
function _cameraPose(runner) {
|
|
631
|
+
var cam = runner.cameraControl;
|
|
632
|
+
if (!cam || typeof cam.getOrbit !== 'function' || !Array.isArray(cam.poseFields)) { return null; }
|
|
633
|
+
var orbit = cam.getOrbit() || {};
|
|
634
|
+
var pose = {};
|
|
635
|
+
cam.poseFields.forEach(function (f) { if (orbit[f.name] !== undefined) { pose[f.name] = _plain(orbit[f.name]); } });
|
|
636
|
+
return pose;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* exp.setDeclared(name, patch) → Promise<{ set, from, to, tick: 0, kept, files }> — change
|
|
641
|
+
* an `inParts` object's declaration and re-run the scene IN THIS DOCUMENT. `patch` keys must be writable fields
|
|
642
|
+
* of the kind's descriptor and each value must pass the descriptor's own validator (ObjectSchema.normalizeValue);
|
|
643
|
+
* otherwise it refuses (throws) naming the key. The held text is patched (patchParts `set`), then the one restart
|
|
644
|
+
* door (runner.restart → the retained replay re-links the held text) re-runs it: the seed (the sketch's own
|
|
645
|
+
* createScene/createCanvas) and the camera pose (captured through the camera's poseFields, re-applied through
|
|
646
|
+
* setOrbit) are kept; `kept` lists 'cameraPose' only when a pose was captured and re-applied. A `rect` /
|
|
647
|
+
* `center` patch sets where the entry STARTS. The loop's run state is kept. REFUSES (throws) while a hand
|
|
648
|
+
* made since the last toSketch is unwritten (write it first: await exp.toSketch()), and a `name` patch (the
|
|
649
|
+
* key is the name). `from` and `to` are the patched keys' capture (ObjectSchema.captureProps) before and after the
|
|
650
|
+
* re-run — one form on both sides (a z-slab wall's rect is 6 numbers even when the patch typed 4); the patch as
|
|
651
|
+
* typed is in `files`. With a Record open, its `setDeclared` row carries the same `from`/`to`, written after the re-run.
|
|
652
|
+
*/
|
|
653
|
+
RecipeContext.prototype.setDeclared = async function (name, patch, opts) {
|
|
654
|
+
_requireWorld(this);
|
|
655
|
+
_requireHeld(this, 'setDeclared');
|
|
656
|
+
var by = this._record ? Record.requireBy(opts && opts.by, 'RecipeContext.setDeclared') : null;
|
|
657
|
+
var runner = this._runner;
|
|
658
|
+
var obj = null;
|
|
659
|
+
this._parts.forEach(function (row, o) { if (row.key === name) { obj = o; } });
|
|
660
|
+
if (!obj || runner.declared.indexOf(obj) === -1) {
|
|
661
|
+
throw new Error('[RecipeContext.setDeclared] "' + name + '" is not an object of this sketch\'s `parts` block' +
|
|
662
|
+
(this._code.size ? ' (code declarations: ' + Array.from(this._code.keys()).map(function (o) { return o.name; }).join(', ') +
|
|
663
|
+
' — ' + OUTSIDE_PARTS + ')' : '') + '. (FAIL-LOUD.)');
|
|
664
|
+
}
|
|
665
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch) || !Object.keys(patch).length) {
|
|
666
|
+
throw new TypeError('[RecipeContext.setDeclared] patch is { <field>: value, … } with at least one field. (FAIL-LOUD.)');
|
|
667
|
+
}
|
|
668
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'name')) {
|
|
669
|
+
throw new TypeError('[RecipeContext.setDeclared] "name": ' + RENAME + ' (FAIL-LOUD.)');
|
|
670
|
+
}
|
|
671
|
+
// A hand made since the last toSketch is not in the held text, and the re-run would drop it without a
|
|
672
|
+
// word: refuse, naming the object, until the host writes it (a placement and a setDeclared are two changes).
|
|
673
|
+
var unwritten = _unwrittenHands(this);
|
|
674
|
+
if (unwritten.length) {
|
|
675
|
+
throw new Error('[RecipeContext.setDeclared] "' + unwritten.join('", "') + '" carries a hand the sketch text does ' +
|
|
676
|
+
'not hold yet — write the hand first: await exp.toSketch(). (FAIL-LOUD.)');
|
|
677
|
+
}
|
|
678
|
+
var kind = runner.kindOf(obj);
|
|
679
|
+
var w = _writable(kind);
|
|
680
|
+
var live = _capture(this, obj);
|
|
681
|
+
var from = {};
|
|
682
|
+
Object.keys(patch).forEach(function (k) {
|
|
683
|
+
if (!w[k]) {
|
|
684
|
+
throw new TypeError('[RecipeContext.setDeclared] "' + k + '" is not a writable field of ' + kind + ' (writable: ' +
|
|
685
|
+
Object.keys(w).join(', ') + '). (FAIL-LOUD.)');
|
|
686
|
+
}
|
|
687
|
+
try { ObjectSchema.normalizeValue(w[k], patch[k], kind); }
|
|
688
|
+
catch (err) {
|
|
689
|
+
throw new TypeError('[RecipeContext.setDeclared] "' + k + '": ' + String((err && err.message) || err) + ' (FAIL-LOUD.)');
|
|
690
|
+
}
|
|
691
|
+
from[k] = live[k] === undefined ? null : live[k];
|
|
692
|
+
});
|
|
693
|
+
var entry = this._project.entry;
|
|
694
|
+
var spelled = _spell(this._project.files[entry], name, kind, _plain(patch), live);
|
|
695
|
+
var r = patchParts(this._project.files[entry], [{ op: 'set', name: name, props: spelled }], { entry: entry });
|
|
696
|
+
if (r.refused.length) {
|
|
697
|
+
throw new Error('[RecipeContext.setDeclared] the sketch text refused the patch: ' +
|
|
698
|
+
r.refused.map(function (x) { return (x.key ? x.key + ': ' : '') + x.reason; }).join('; ') + ' (FAIL-LOUD.)');
|
|
699
|
+
}
|
|
700
|
+
var files = Object.assign({}, this._project.files);
|
|
701
|
+
files[entry] = r.text;
|
|
702
|
+
this._project = Object.assign({}, this._project, { files: files });
|
|
703
|
+
var pose = _cameraPose(runner);
|
|
704
|
+
var wasRunning = !!runner._isLooping;
|
|
705
|
+
await runner.restart();
|
|
706
|
+
if (!wasRunning) { runner.stop(); }
|
|
707
|
+
// `kept` is what WAS kept: the seed by construction (the sketch's own co-init cfg); the camera pose only
|
|
708
|
+
// when one was captured and re-applied (a 2D scene with no pose camera has none).
|
|
709
|
+
var kept = ['seed'];
|
|
710
|
+
if (pose && runner.cameraControl && typeof runner.cameraControl.setOrbit === 'function') {
|
|
711
|
+
runner.cameraControl.setOrbit(pose);
|
|
712
|
+
kept.push('cameraPose');
|
|
713
|
+
}
|
|
714
|
+
// `to` is what the re-run declared, in the capture form `from` is in (the patch as typed is in the text).
|
|
715
|
+
var rebuilt = null;
|
|
716
|
+
this._parts.forEach(function (row, o) { if (row.key === name) { rebuilt = o; } });
|
|
717
|
+
if (!rebuilt) {
|
|
718
|
+
throw new Error('[RecipeContext.setDeclared] the re-run declared no "' + name + '" — the patched text dropped the entry. (FAIL-LOUD.)');
|
|
719
|
+
}
|
|
720
|
+
var after = _capture(this, rebuilt);
|
|
721
|
+
var to = {};
|
|
722
|
+
Object.keys(patch).forEach(function (k) { to[k] = after[k] === undefined ? null : _plain(after[k]); });
|
|
723
|
+
if (this._record) { this._record._row({ name: name, op: 'setDeclared', by: by, from: _plain(from), to: _plain(to) }); }
|
|
724
|
+
return { set: name, from: from, to: to, tick: 0, kept: kept, files: Object.assign({}, files) };
|
|
725
|
+
};
|
|
726
|
+
|
|
201
727
|
RecipeContext.prototype.addTickFunction = function (fn) {
|
|
202
728
|
_requireWorld(this);
|
|
203
729
|
var fns = this._runner.universe.experimentTickFunctions;
|
|
@@ -259,24 +785,106 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
259
785
|
* replay on the runner (`runner.retainSceneReplay`), so `runner.restart()` — and a sketch's
|
|
260
786
|
* declared `restart` control — replays this build on the same runner and canvas.
|
|
261
787
|
*/
|
|
262
|
-
RecipeContext.prototype.build = function (
|
|
263
|
-
|
|
264
|
-
|
|
788
|
+
RecipeContext.prototype.build = function (sketch) {
|
|
789
|
+
var linked = null;
|
|
790
|
+
if (sketch && typeof sketch === 'object' && typeof sketch.buildExperiment === 'function') {
|
|
791
|
+
if (!sketch.project || typeof sketch.project !== 'object' || !sketch.project.files || typeof sketch.project.entry !== 'string') {
|
|
792
|
+
throw new TypeError('[RecipeContext.build] build(linked): linked is linkProject\'s result (its contract: ' +
|
|
793
|
+
'lib/shim/project/link.js). (FAIL-LOUD.)');
|
|
794
|
+
}
|
|
795
|
+
linked = sketch;
|
|
796
|
+
sketch = linked.buildExperiment;
|
|
797
|
+
} else if (sketch !== undefined && sketch !== null && typeof sketch !== 'function') {
|
|
798
|
+
throw new TypeError('[RecipeContext.build] build(sketch): sketch is the scene\'s buildExperiment(exp) function or ' +
|
|
799
|
+
'linkProject\'s result (got ' + typeof sketch + '). (FAIL-LOUD.)');
|
|
800
|
+
}
|
|
801
|
+
if (typeof sketch === 'function') {
|
|
802
|
+
// ONE Record per booted world: a build of text that is not the held text re-authors it, and closes the
|
|
803
|
+
// open Record with the world before (a rebuild of the same text — restart, setDeclared's re-run — does not).
|
|
804
|
+
if (this._record && !(linked && this._project && _sameFiles(linked.project.files, this._project.files))) {
|
|
805
|
+
this._reauthored = this._record;
|
|
806
|
+
this._record._closeOnReauthor(this._project);
|
|
807
|
+
}
|
|
808
|
+
this._buildFn = sketch;
|
|
809
|
+
this._project = linked ? linked.project : null;
|
|
810
|
+
this._urls = linked ? (linked.urls || null) : null;
|
|
265
811
|
if (this._runner) { _teardownSceneForRebuild(this); }
|
|
266
812
|
this._world = null;
|
|
267
|
-
|
|
813
|
+
this._parts = new Map();
|
|
814
|
+
this._code = new Map();
|
|
815
|
+
this._declaredOnce = false;
|
|
816
|
+
sketch(this);
|
|
268
817
|
if (!this._world) {
|
|
269
818
|
throw new Error('[RecipeContext.build] the scene never created its world — its first statement ' +
|
|
270
819
|
'must be ' + COINIT + '. (FAIL-LOUD.)');
|
|
271
820
|
}
|
|
272
821
|
this._built = true;
|
|
273
822
|
var self = this;
|
|
274
|
-
this._runner.
|
|
823
|
+
this._runner.declared.forEach(function (obj) {
|
|
824
|
+
if (!self._parts.has(obj)) { self._code.set(obj, _capture(self, obj)); }
|
|
825
|
+
});
|
|
826
|
+
this._runner.retainSceneReplay(this._project
|
|
827
|
+
? function () { return linkProject(self._project).then(function (l) { return self.build(l); }); }
|
|
828
|
+
: function () { return self.build(self._buildFn); });
|
|
275
829
|
}
|
|
276
830
|
setPageExperiment(this);
|
|
277
831
|
return this;
|
|
278
832
|
};
|
|
279
833
|
|
|
834
|
+
/** Two file maps hold the same paths with byte-equal text. */
|
|
835
|
+
function _sameFiles(a, b) {
|
|
836
|
+
var ka = Object.keys(a), kb = Object.keys(b);
|
|
837
|
+
if (ka.length !== kb.length) { return false; }
|
|
838
|
+
return ka.every(function (k) { return Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]; });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* exp.startRecord() → Promise<Record> — open a Record (lib/frqtl/objects/record/Record.js) on this facade's world.
|
|
843
|
+
* REFUSES (rejects): a second open Record (one per facade, one per runner); a held project with no frqtl.json
|
|
844
|
+
* (a Record snapshots a folder); a hand the sketch text does not hold yet (as setDeclared). While it is open,
|
|
845
|
+
* toSketch({ by }) and setDeclared(name, patch, { by }) name who acted.
|
|
846
|
+
*/
|
|
847
|
+
RecipeContext.prototype.startRecord = async function () {
|
|
848
|
+
_requireWorld(this);
|
|
849
|
+
_requireHeld(this, 'startRecord');
|
|
850
|
+
if (this._record) {
|
|
851
|
+
throw new Error('[RecipeContext.startRecord] a Record is already open on this facade (one per booted world) — ' +
|
|
852
|
+
'await rec.stop() first. (FAIL-LOUD.)');
|
|
853
|
+
}
|
|
854
|
+
// A re-authoring closed the previous Record; if that close was REFUSED and nobody read it, its rows would be lost
|
|
855
|
+
// without a word: refuse until the host reads `rec.closed` (the rejection carries err.record).
|
|
856
|
+
var last = this._reauthored;
|
|
857
|
+
if (last) {
|
|
858
|
+
await last._settled;
|
|
859
|
+
if (last._refused && !last._closedRead) {
|
|
860
|
+
var err = new Error('[RecipeContext.startRecord] the previous Record of this facade was closed by a re-authoring and ' +
|
|
861
|
+
'REFUSED (' + last._refused.message + ') — nobody read rec.closed, so its ' + last._refused.record.changes.length +
|
|
862
|
+
' row(s) would be lost: read rec.closed (its rejection carries err.record = { changes, clips }) first. (FAIL-LOUD.)');
|
|
863
|
+
err.record = last._refused.record;
|
|
864
|
+
throw err;
|
|
865
|
+
}
|
|
866
|
+
this._reauthored = null;
|
|
867
|
+
}
|
|
868
|
+
if (openRecordOf(this._runner)) {
|
|
869
|
+
throw new Error('[RecipeContext.startRecord] another facade holds an open Record on this runner. (FAIL-LOUD.)');
|
|
870
|
+
}
|
|
871
|
+
if (!Object.prototype.hasOwnProperty.call(this._project.files, MANIFEST_FILE)) {
|
|
872
|
+
throw new Error('[RecipeContext.startRecord] a Record snapshots a folder; boot it as one — the held project carries ' +
|
|
873
|
+
'no ' + MANIFEST_FILE + ': add files[\'' + MANIFEST_FILE + '\'] = serializeManifest(manifestFor({ name, entry: SKETCH_ENTRY })) ' +
|
|
874
|
+
'(all from ./project) to the files you boot. (FAIL-LOUD.)');
|
|
875
|
+
}
|
|
876
|
+
var unwritten = _unwrittenHands(this);
|
|
877
|
+
if (unwritten.length) {
|
|
878
|
+
throw new Error('[RecipeContext.startRecord] "' + unwritten.join('", "') + '" carries a hand the sketch text does ' +
|
|
879
|
+
'not hold yet — write the hand first: await exp.toSketch(). (FAIL-LOUD.)');
|
|
880
|
+
}
|
|
881
|
+
var rec = new Record(this);
|
|
882
|
+
this._record = rec;
|
|
883
|
+
try { await rec._ready(this._project); }
|
|
884
|
+
catch (e) { rec._detach(); throw e; }
|
|
885
|
+
return rec;
|
|
886
|
+
};
|
|
887
|
+
|
|
280
888
|
RecipeContext.prototype.run = function (buildFn) {
|
|
281
889
|
if (buildFn && typeof buildFn === 'function' && !this._built) { this.build(buildFn); }
|
|
282
890
|
_requireWorld(this);
|
|
@@ -298,7 +906,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
|
|
|
298
906
|
var RETIRED_RECIPE_DOORS = Object.freeze({
|
|
299
907
|
buildSource: '[RecipeContext] exp.buildSource(src) is retired — a sketch held as text is a project: boot it ' +
|
|
300
908
|
'with bootProject(mount, { files: { "sketch.js": src }, entry: "sketch.js" }) (@descix/frqtl-web/boot), or ' +
|
|
301
|
-
're-scene a booted facade with exp.build(
|
|
909
|
+
're-scene a booted facade with exp.build(await linkProject({ files, entry, mint })) ' +
|
|
302
910
|
'(lib/shim/project/link.js). (FAIL-LOUD.)'
|
|
303
911
|
});
|
|
304
912
|
refuseRetiredDoors(RecipeContext.prototype, RETIRED_RECIPE_DOORS);
|