@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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* record-format.js — THE owner of the Record folder's shape (the sketch-projects design §6), and its ONE writer.
|
|
5
|
+
*
|
|
6
|
+
* A Record is its own folder, and it contains the project it recorded:
|
|
7
|
+
*
|
|
8
|
+
* record.json { tickRange: [first, last], clips: ['clip-01.mp4', …], changes: [{tick, name, op, by, from, to}] }
|
|
9
|
+
* clip-01.mp4 … the FrqtlVision footage, one file per CLIP_SECONDS segment, in the order filmed
|
|
10
|
+
* parts.before.json { "<name>": ["<registered kind name>", {props}] } at Record start, in key order
|
|
11
|
+
* parts.after.json the same at stop; their diff IS the code change
|
|
12
|
+
* project/… the project's files at stop (frqtl.json, sketch.js, parts/*, README), replayable
|
|
13
|
+
* through bootProject
|
|
14
|
+
*
|
|
15
|
+
* A host's own files (a lab's lab.json) may sit beside these; this module never reads or checks them.
|
|
16
|
+
*
|
|
17
|
+
* RECORD_FILES the folder's §6 paths (record, partsBefore, partsAfter, project)
|
|
18
|
+
* RECORD_KEYS record.json's keys, exactly — there is no record-level who
|
|
19
|
+
* CHANGE_KEYS one change row's keys, exactly
|
|
20
|
+
* OPS the change ops: a placement is `move` whatever hand made it
|
|
21
|
+
* BY who acted, per row
|
|
22
|
+
* CLIP_SECONDS { min, max } — the length of one clip, in seconds of playback
|
|
23
|
+
* CLIP_NAME the clip file pattern; clipName(n) → 'clip-NN.mp4' (n from 1)
|
|
24
|
+
* jsonText(v) JSON.stringify(v, null, 2) + '\n' — every JSON file of the folder
|
|
25
|
+
* recordFolderFaults(files) → string[] [] for a complete §6 folder, every fault by name otherwise
|
|
26
|
+
* serializeRecordFolder(parts) → { files, record, clips } | { empty: true } THE writer (Record.js calls it;
|
|
27
|
+
* it runs recordFolderFaults on its own output and THROWS on any fault)
|
|
28
|
+
*
|
|
29
|
+
* `files` is a tree { path → string | Uint8Array }: JSON and project text as strings (or UTF-8 bytes), clips
|
|
30
|
+
* as bytes. Values in §6 files are strict JSON, in record.json's own vocabulary; a placeholder ("*",
|
|
31
|
+
* {unavailable}) never sits in a §6 field.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { MANIFEST_FILE } from './manifest.js';
|
|
35
|
+
import { entryOf } from './link.js';
|
|
36
|
+
|
|
37
|
+
/** The folder's §6 paths. */
|
|
38
|
+
export const RECORD_FILES = Object.freeze({
|
|
39
|
+
record: 'record.json',
|
|
40
|
+
partsBefore: 'parts.before.json',
|
|
41
|
+
partsAfter: 'parts.after.json',
|
|
42
|
+
project: 'project/'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** record.json's keys, exactly. */
|
|
46
|
+
export const RECORD_KEYS = Object.freeze(['tickRange', 'clips', 'changes']);
|
|
47
|
+
|
|
48
|
+
/** One change row's keys, exactly. */
|
|
49
|
+
export const CHANGE_KEYS = Object.freeze(['tick', 'name', 'op', 'by', 'from', 'to']);
|
|
50
|
+
|
|
51
|
+
/** The change ops. */
|
|
52
|
+
export const OPS = Object.freeze(['move', 'setDeclared', 'add', 'remove', 'fork']);
|
|
53
|
+
|
|
54
|
+
/** Who acted (each row's `by`). */
|
|
55
|
+
export const BY = Object.freeze(['human', 'maxi']);
|
|
56
|
+
|
|
57
|
+
/** The length of one clip, in seconds of playback (frames / fps). */
|
|
58
|
+
export const CLIP_SECONDS = Object.freeze({ min: 5, max: 10 });
|
|
59
|
+
|
|
60
|
+
/** The clip file pattern. */
|
|
61
|
+
export const CLIP_NAME = /^clip-(\d{2})\.mp4$/;
|
|
62
|
+
|
|
63
|
+
/** @param {number} n 1-based @returns {string} 'clip-NN.mp4' */
|
|
64
|
+
export function clipName(n) {
|
|
65
|
+
if (!Number.isInteger(n) || n < 1 || n > 99) {
|
|
66
|
+
throw new Error('[record-format] a clip number is a whole number 1–99, got ' + JSON.stringify(n) + '. (FAIL-LOUD.)');
|
|
67
|
+
}
|
|
68
|
+
return 'clip-' + String(n).padStart(2, '0') + '.mp4';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** THE text of every JSON file of a Record folder. */
|
|
72
|
+
export function jsonText(v) {
|
|
73
|
+
return JSON.stringify(v, null, 2) + '\n';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── value shapes ────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const POSITION_KEYS = Object.freeze({ center: [3], rect: [4, 6] });
|
|
79
|
+
const FORK_FILE = /^parts\/[^/]+\.js$/;
|
|
80
|
+
|
|
81
|
+
function _isPlain(v) {
|
|
82
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
83
|
+
(Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The first path inside `v` that is not strict JSON (a function, NaN, undefined, a class instance …), else null. */
|
|
87
|
+
function _strictJsonFault(v, at) {
|
|
88
|
+
if (v === null || typeof v === 'string' || typeof v === 'boolean') { return null; }
|
|
89
|
+
if (typeof v === 'number') { return Number.isFinite(v) ? null : at + ' is ' + String(v); }
|
|
90
|
+
if (Array.isArray(v)) {
|
|
91
|
+
for (let i = 0; i < v.length; i++) { const f = _strictJsonFault(v[i], at + '[' + i + ']'); if (f) { return f; } }
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (_isPlain(v)) {
|
|
95
|
+
for (const k of Object.keys(v)) { const f = _strictJsonFault(v[k], at + '.' + k); if (f) { return f; } }
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return at + ' is ' + (v === undefined ? 'undefined' : typeof v === 'object' ? 'a ' + ((v.constructor && v.constructor.name) || 'class') + ' instance' : 'a ' + typeof v);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every placeholder inside `v`: an {unavailable} object or a "*" string. */
|
|
102
|
+
function _placeholders(v, at, out) {
|
|
103
|
+
if (v === '*') { out.push(at + ' is the placeholder "*"'); return out; }
|
|
104
|
+
if (Array.isArray(v)) { v.forEach(function (x, i) { _placeholders(x, at + '[' + i + ']', out); }); return out; }
|
|
105
|
+
if (v && typeof v === 'object') {
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(v, 'unavailable')) { out.push(at + ' is the placeholder {unavailable}'); return out; }
|
|
107
|
+
Object.keys(v).forEach(function (k) { _placeholders(v[k], at + '.' + k, out); });
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _isEntry(e) {
|
|
113
|
+
return Array.isArray(e) && e.length === 2 && typeof e[0] === 'string' && e[0].length > 0 && _isPlain(e[1]) && !_strictJsonFault(e[1], '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** { key, length } of a move value `{center: [3]}` / `{rect: [4|6]}`, else null. */
|
|
117
|
+
function _placement(v) {
|
|
118
|
+
if (!_isPlain(v)) { return null; }
|
|
119
|
+
const ks = Object.keys(v);
|
|
120
|
+
if (ks.length !== 1 || !POSITION_KEYS[ks[0]]) { return null; }
|
|
121
|
+
const a = v[ks[0]];
|
|
122
|
+
if (!Array.isArray(a) || POSITION_KEYS[ks[0]].indexOf(a.length) === -1 || !a.every(function (n) { return typeof n === 'number' && Number.isFinite(n); })) { return null; }
|
|
123
|
+
return { key: ks[0], length: a.length };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const MOVE_SHAPE = 'move carries from/to as ONE key, {center: [x, y, z]} or {rect: [4 numbers flat | 6 with a z-extent]}, the same key and length in both';
|
|
127
|
+
|
|
128
|
+
/** The faults of one change row against its op. */
|
|
129
|
+
function _rowFaults(c, at) {
|
|
130
|
+
const bad = [];
|
|
131
|
+
if (!_isPlain(c)) { return [at + ' is not a change {' + CHANGE_KEYS.join(', ') + '}']; }
|
|
132
|
+
const keys = Object.keys(c);
|
|
133
|
+
if (keys.length !== CHANGE_KEYS.length || !CHANGE_KEYS.every(function (k) { return keys.indexOf(k) !== -1; })) {
|
|
134
|
+
bad.push(at + ' carries exactly {' + CHANGE_KEYS.join(', ') + '}, got {' + keys.join(', ') + '}');
|
|
135
|
+
}
|
|
136
|
+
if (!Number.isInteger(c.tick) || c.tick < 0) { bad.push(at + '.tick is a whole tick ≥ 0, got ' + JSON.stringify(c.tick)); }
|
|
137
|
+
if (typeof c.name !== 'string' || !c.name) { bad.push(at + '.name is the declared name, got ' + JSON.stringify(c.name)); }
|
|
138
|
+
if (BY.indexOf(c.by) === -1) { bad.push(at + '.by is one of ' + BY.join('|') + ', got ' + JSON.stringify(c.by)); }
|
|
139
|
+
['from', 'to'].forEach(function (k) {
|
|
140
|
+
const f = _strictJsonFault(c[k], at + '.' + k);
|
|
141
|
+
if (f) { bad.push(f + ' — from/to are strict JSON'); }
|
|
142
|
+
});
|
|
143
|
+
if (c.op === 'move') {
|
|
144
|
+
const a = _placement(c.from), b = _placement(c.to);
|
|
145
|
+
if (!a || !b || a.key !== b.key || a.length !== b.length) { bad.push(at + ': ' + MOVE_SHAPE + ', got ' + JSON.stringify([c.from, c.to])); }
|
|
146
|
+
else if (JSON.stringify(c.from) === JSON.stringify(c.to)) { bad.push(at + ' is a zero move of "' + c.name + '" — an act that moved nothing writes no row'); }
|
|
147
|
+
} else if (c.op === 'setDeclared') {
|
|
148
|
+
if (!_isPlain(c.from) || !_isPlain(c.to) || !Object.keys(c.to).length ||
|
|
149
|
+
JSON.stringify(Object.keys(c.from).sort()) !== JSON.stringify(Object.keys(c.to).sort())) {
|
|
150
|
+
bad.push(at + ': setDeclared carries from/to as {key: value} of the patched keys, the same keys before and after, got ' + JSON.stringify([c.from, c.to]));
|
|
151
|
+
}
|
|
152
|
+
} else if (c.op === 'add') {
|
|
153
|
+
if (c.from !== null || !_isEntry(c.to)) { bad.push(at + ': add carries from: null and to: ["<registered kind name>", {props}], got ' + JSON.stringify([c.from, c.to])); }
|
|
154
|
+
} else if (c.op === 'remove') {
|
|
155
|
+
if (c.to !== null || !_isEntry(c.from)) { bad.push(at + ': remove carries from: ["<registered kind name>", {props}] and to: null, got ' + JSON.stringify([c.from, c.to])); }
|
|
156
|
+
} else if (c.op === 'fork') {
|
|
157
|
+
const ok = typeof c.from === 'string' && c.from.length > 0 && _isPlain(c.to) &&
|
|
158
|
+
JSON.stringify(Object.keys(c.to).sort()) === '["file","kind"]' &&
|
|
159
|
+
typeof c.to.kind === 'string' && c.to.kind.length > 0 && typeof c.to.file === 'string' && FORK_FILE.test(c.to.file);
|
|
160
|
+
if (!ok) { bad.push(at + ': fork carries from: "<source kind name>" and to: {"kind": "<new kind name>", "file": "parts/<File>.js"}, got ' + JSON.stringify([c.from, c.to])); }
|
|
161
|
+
} else {
|
|
162
|
+
bad.push(at + '.op ' + JSON.stringify(c.op) + ' is not one of ' + OPS.join('|') + ' (a placement is "move" whatever hand made it)');
|
|
163
|
+
}
|
|
164
|
+
return bad;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _text(v) {
|
|
168
|
+
return typeof v === 'string' ? v : new TextDecoder('utf-8', { fatal: true }).decode(v);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _isBytes(v) { return v instanceof Uint8Array; }
|
|
172
|
+
|
|
173
|
+
/** Parse a JSON file of the tree: { value } | { fault }. */
|
|
174
|
+
function _json(files, path) {
|
|
175
|
+
if (!Object.prototype.hasOwnProperty.call(files, path)) { return { fault: 'the folder has no ' + path }; }
|
|
176
|
+
try { return { value: JSON.parse(_text(files[path])) }; }
|
|
177
|
+
catch (e) { return { fault: path + ' is not JSON: ' + ((e && e.message) || String(e)) }; }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* recordFolderFaults(files) → string[] — [] for a COMPLETE §6 Record folder, otherwise every fault by name.
|
|
182
|
+
* §6 checks only: record.json's keys and value shapes; each row against its op (the fork shape included);
|
|
183
|
+
* ticks inside tickRange and never decreasing; no zero move; one shape per name's moves; `clips` equal to the
|
|
184
|
+
* clip files present, in order; the parts.*.json entries; `project/frqtl.json` naming an entry present under
|
|
185
|
+
* project/. A host's own files (lab.json …) are never read.
|
|
186
|
+
* @param {Object<string, string|Uint8Array>} files
|
|
187
|
+
* @returns {string[]}
|
|
188
|
+
*/
|
|
189
|
+
export function recordFolderFaults(files) {
|
|
190
|
+
if (!_isPlain(files)) { return ['a Record folder is a tree { path: string | Uint8Array }']; }
|
|
191
|
+
const bad = [];
|
|
192
|
+
Object.keys(files).forEach(function (p) {
|
|
193
|
+
if (typeof files[p] !== 'string' && !_isBytes(files[p])) { bad.push(p + ' is neither text nor bytes'); }
|
|
194
|
+
});
|
|
195
|
+
// record.json
|
|
196
|
+
const r = _json(files, RECORD_FILES.record);
|
|
197
|
+
if (r.fault) { bad.push(r.fault); }
|
|
198
|
+
const rec = r.value;
|
|
199
|
+
let range = null;
|
|
200
|
+
let clips = [];
|
|
201
|
+
if (rec !== undefined) {
|
|
202
|
+
if (!_isPlain(rec)) { bad.push(RECORD_FILES.record + ' is an object {' + RECORD_KEYS.join(', ') + '}'); }
|
|
203
|
+
else {
|
|
204
|
+
const keys = Object.keys(rec);
|
|
205
|
+
if (keys.length !== RECORD_KEYS.length || !RECORD_KEYS.every(function (k) { return keys.indexOf(k) !== -1; })) {
|
|
206
|
+
bad.push(RECORD_FILES.record + ' carries exactly {' + RECORD_KEYS.join(', ') + '} (no record-level who: each change names its `by`), got {' + keys.join(', ') + '}');
|
|
207
|
+
}
|
|
208
|
+
_placeholders(rec, RECORD_FILES.record, []).forEach(function (f) { bad.push(f + ' — a placeholder never sits in a §6 field'); });
|
|
209
|
+
const tr = rec.tickRange;
|
|
210
|
+
if (!Array.isArray(tr) || tr.length !== 2 || !tr.every(function (t) { return Number.isInteger(t) && t >= 0; }) || tr[1] < tr[0]) {
|
|
211
|
+
bad.push(RECORD_FILES.record + ' tickRange is [first, last] whole ticks, first ≤ last, got ' + JSON.stringify(tr));
|
|
212
|
+
} else { range = tr; }
|
|
213
|
+
if (!Array.isArray(rec.clips)) { bad.push(RECORD_FILES.record + ' clips is the list of clip files, got ' + JSON.stringify(rec.clips)); }
|
|
214
|
+
else {
|
|
215
|
+
clips = rec.clips;
|
|
216
|
+
clips.forEach(function (c, i) {
|
|
217
|
+
const want = clipName(i + 1);
|
|
218
|
+
if (c !== want) { bad.push(RECORD_FILES.record + ' clips[' + i + '] is ' + JSON.stringify(c) + ', not ' + want + ' (clips in the order filmed)'); }
|
|
219
|
+
else if (!_isBytes(files[c]) || !files[c].length) { bad.push(c + ' is listed in ' + RECORD_FILES.record + ' clips but is not in the folder as bytes'); }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(rec.changes)) { bad.push(RECORD_FILES.record + ' changes is the list of change rows, got ' + JSON.stringify(rec.changes)); }
|
|
223
|
+
else {
|
|
224
|
+
const shapeOf = {};
|
|
225
|
+
let last = -Infinity;
|
|
226
|
+
rec.changes.forEach(function (c, i) {
|
|
227
|
+
const at = RECORD_FILES.record + ' changes[' + i + ']';
|
|
228
|
+
_rowFaults(c, at).forEach(function (f) { bad.push(f); });
|
|
229
|
+
if (!_isPlain(c)) { return; }
|
|
230
|
+
if (Number.isInteger(c.tick)) {
|
|
231
|
+
if (range && (c.tick < range[0] || c.tick > range[1])) { bad.push(at + ' at tick ' + c.tick + ' lies outside tickRange ' + JSON.stringify(range)); }
|
|
232
|
+
if (c.tick < last) { bad.push(at + ' at tick ' + c.tick + ' comes after a row at tick ' + last + ' — rows are in the order acted, so ticks only climb'); }
|
|
233
|
+
last = Math.max(last, c.tick);
|
|
234
|
+
}
|
|
235
|
+
if (c.op === 'move') {
|
|
236
|
+
const p = _placement(c.from);
|
|
237
|
+
if (p) {
|
|
238
|
+
const s = p.key + '/' + p.length;
|
|
239
|
+
if (shapeOf[c.name] && shapeOf[c.name] !== s) {
|
|
240
|
+
bad.push(at + ' moves "' + c.name + '" as {' + p.key + ': ' + p.length + ' numbers} after a row that moved it as {' +
|
|
241
|
+
shapeOf[c.name].replace('/', ': ') + ' numbers} — one part moves in the one shape of its position authority');
|
|
242
|
+
}
|
|
243
|
+
shapeOf[c.name] = shapeOf[c.name] || s;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
Object.keys(files).filter(function (f) { return CLIP_NAME.test(f) && clips.indexOf(f) === -1; }).sort().forEach(function (f) {
|
|
251
|
+
bad.push(f + ' is in the folder but not listed in ' + RECORD_FILES.record + ' clips');
|
|
252
|
+
});
|
|
253
|
+
// parts.before.json / parts.after.json
|
|
254
|
+
[RECORD_FILES.partsBefore, RECORD_FILES.partsAfter].forEach(function (p) {
|
|
255
|
+
const j = _json(files, p);
|
|
256
|
+
if (j.fault) { bad.push(j.fault); return; }
|
|
257
|
+
if (!_isPlain(j.value)) { bad.push(p + ' is {"<name>": ["<registered kind name>", {props}]}, got ' + JSON.stringify(j.value)); return; }
|
|
258
|
+
_placeholders(j.value, p, []).forEach(function (f) { bad.push(f + ' — a placeholder never sits in a §6 field'); });
|
|
259
|
+
Object.keys(j.value).forEach(function (n) {
|
|
260
|
+
if (!_isEntry(j.value[n])) { bad.push(p + ' "' + n + '" is not ["<registered kind name>", {props}]: ' + JSON.stringify(j.value[n])); }
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
// project/
|
|
264
|
+
const mp = RECORD_FILES.project + MANIFEST_FILE;
|
|
265
|
+
if (!Object.prototype.hasOwnProperty.call(files, mp)) { bad.push('project/ has no ' + MANIFEST_FILE + ' naming its entry (a Record snapshots a project folder)'); }
|
|
266
|
+
else {
|
|
267
|
+
let entry = null;
|
|
268
|
+
try { entry = entryOf({ [MANIFEST_FILE]: _text(files[mp]) }); }
|
|
269
|
+
catch (e) { bad.push(mp + ' is refused by the entry rule (link.js entryOf): ' + ((e && e.message) || String(e))); }
|
|
270
|
+
if (entry !== null && !Object.prototype.hasOwnProperty.call(files, RECORD_FILES.project + entry)) {
|
|
271
|
+
bad.push('project/ has no ' + entry + ' (the entry ' + MANIFEST_FILE + ' names)');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return bad;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── the writer ──────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/** A parts block → its §6 JSON value, refusing a value that is not strict JSON by name. */
|
|
280
|
+
function _partsValue(block, which) {
|
|
281
|
+
if (!_isPlain(block)) { throw new TypeError('[record-format] ' + which + ' is { "<name>": ["<registered kind name>", {props}] }. (FAIL-LOUD.)'); }
|
|
282
|
+
const out = {};
|
|
283
|
+
Object.keys(block).forEach(function (n) {
|
|
284
|
+
const e = block[n];
|
|
285
|
+
if (!Array.isArray(e) || e.length !== 2 || typeof e[0] !== 'string' || !e[0] || !_isPlain(e[1])) {
|
|
286
|
+
throw new TypeError('[record-format] ' + which + ' "' + n + '" is not ["<registered kind name>", {props}]. (FAIL-LOUD.)');
|
|
287
|
+
}
|
|
288
|
+
const f = _strictJsonFault(e[1], n);
|
|
289
|
+
if (f) {
|
|
290
|
+
throw new TypeError('[record-format] ' + which + ': ' + f + ' — a Record writes props as strict JSON; declare that value ' +
|
|
291
|
+
'as a literal in `parts`. (FAIL-LOUD.)');
|
|
292
|
+
}
|
|
293
|
+
out[n] = [e[0], JSON.parse(JSON.stringify(e[1]))];
|
|
294
|
+
});
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export const FILM_AFTER_LAST_CHANGE = 'film after your last change: the footage must show every change';
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* serializeRecordFolder({ changes, clips, partsBefore, partsAfter, project }) — THE writer of a Record folder.
|
|
302
|
+
* changes the rows, in the order acted
|
|
303
|
+
* clips [{ bytes: Uint8Array, firstTick, lastTick }], in the order filmed (Record ticks)
|
|
304
|
+
* partsBefore / partsAfter { name: [registered kind name, props] }
|
|
305
|
+
* project { path: text } — the held files at stop
|
|
306
|
+
* → { empty: true } when there is no change and no clip (a state, with no folder);
|
|
307
|
+
* → { files, record, clips: [{ file, firstTick, lastTick }] } otherwise.
|
|
308
|
+
* REFUSES (throws): a change and no clip, or a change after the last clip ends (FILM_AFTER_LAST_CHANGE); a
|
|
309
|
+
* props value that is not strict JSON (naming its key); and any recordFolderFaults of its own output.
|
|
310
|
+
*/
|
|
311
|
+
export function serializeRecordFolder(p) {
|
|
312
|
+
const changes = p.changes, clips = p.clips;
|
|
313
|
+
if (!changes.length && !clips.length) { return { empty: true }; }
|
|
314
|
+
if (!clips.length) {
|
|
315
|
+
throw new Error('[record-format] this Record has ' + changes.length + ' change(s) and no clip — ' + FILM_AFTER_LAST_CHANGE + '. (FAIL-LOUD.)');
|
|
316
|
+
}
|
|
317
|
+
const end = clips[clips.length - 1].lastTick;
|
|
318
|
+
const late = changes.filter(function (c) { return c.tick > end; });
|
|
319
|
+
if (late.length) {
|
|
320
|
+
throw new Error('[record-format] ' + late.length + ' change(s) after the last clip ends (Record tick ' + end + '): ' +
|
|
321
|
+
late.map(function (c) { return c.op + ' ' + c.name + ' at tick ' + c.tick; }).join('; ') + ' — ' + FILM_AFTER_LAST_CHANGE + '. (FAIL-LOUD.)');
|
|
322
|
+
}
|
|
323
|
+
const first = changes.reduce(function (m, c) { return Math.min(m, c.tick); }, clips[0].firstTick);
|
|
324
|
+
const record = {
|
|
325
|
+
tickRange: [first, end],
|
|
326
|
+
clips: clips.map(function (c, i) { return clipName(i + 1); }),
|
|
327
|
+
changes: changes.map(function (c) { const o = {}; CHANGE_KEYS.forEach(function (k) { o[k] = c[k]; }); return o; })
|
|
328
|
+
};
|
|
329
|
+
// Check the rows as HELD, before any text: JSON.stringify writes NaN/Infinity as null and drops an undefined key,
|
|
330
|
+
// so a check of the written text would read a different value from the one the Record held.
|
|
331
|
+
const held = [];
|
|
332
|
+
record.changes.forEach(function (c, i) { held.push.apply(held, _rowFaults(c, 'changes[' + i + ']')); });
|
|
333
|
+
if (held.length) { throw new Error('[record-format] a change row is not strict JSON: ' + held.join('; ') + ' (FAIL-LOUD.)'); }
|
|
334
|
+
const files = {};
|
|
335
|
+
files[RECORD_FILES.record] = jsonText(record);
|
|
336
|
+
clips.forEach(function (c, i) { files[clipName(i + 1)] = c.bytes; });
|
|
337
|
+
files[RECORD_FILES.partsBefore] = jsonText(_partsValue(p.partsBefore, RECORD_FILES.partsBefore));
|
|
338
|
+
files[RECORD_FILES.partsAfter] = jsonText(_partsValue(p.partsAfter, RECORD_FILES.partsAfter));
|
|
339
|
+
Object.keys(p.project).sort().forEach(function (f) { files[RECORD_FILES.project + f] = p.project[f]; });
|
|
340
|
+
const faults = recordFolderFaults(files);
|
|
341
|
+
if (faults.length) {
|
|
342
|
+
throw new Error('[record-format] the Record folder this producer wrote is not a complete §6 Record:\n- ' + faults.join('\n- ') + '\n(FAIL-LOUD.)');
|
|
343
|
+
}
|
|
344
|
+
return { files: files, record: record, clips: clips.map(function (c, i) { return { file: clipName(i + 1), firstTick: c.firstTick, lastTick: c.lastTick }; }) };
|
|
345
|
+
}
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* is a PREFIX row (one part = one file under it)
|
|
8
8
|
* libPathOf(specifier) → the lib-identity path it names; THROWS on an unknown specifier
|
|
9
9
|
* importMapFor(libRootUrl) → { imports } — the rows as an import map, every libPath appended to
|
|
10
|
-
* the URL
|
|
11
|
-
*
|
|
12
|
-
* a
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* the URL of the lib-identity root AS SEEN FROM THE PAGE: an absolute
|
|
11
|
+
* URL or one starting with '/', './' or '../' (any other root THROWS —
|
|
12
|
+
* an import map ignores such a value as a bare specifier); prefix rows
|
|
13
|
+
* stay prefixes. No filesystem computation, so a vendored dist root
|
|
14
|
+
* takes the same call.
|
|
15
|
+
* FRQTL_WEB_HOST_SPECIFIERS rows { specifier, libPath, why } a HOST PAGE imports and a sketch never does:
|
|
16
|
+
* ./boot (FRQTL_WEB_BOOT, the scene doors), ./registry and ./project. libPathOf
|
|
17
|
+
* refuses every one of them, so a sketch cannot boot a page.
|
|
18
|
+
* pageImportMapFor(libRootUrl) → importMapFor(libRootUrl) plus every FRQTL_WEB_HOST_SPECIFIERS row: the map
|
|
19
|
+
* of a page that boots a scene through the doors and runs sketches beside it.
|
|
18
20
|
*
|
|
19
21
|
* Inside lib/ the specifier is a SELF-REFERENCE — the package naming itself, the way node resolves a
|
|
20
22
|
* package importing itself by name — not a lib→sdk edge. It is legal ONLY in sketch content
|
|
@@ -47,7 +49,17 @@ export const FRQTL_WEB_SPECIFIERS = Object.freeze([
|
|
|
47
49
|
]);
|
|
48
50
|
|
|
49
51
|
/** The scene doors a host page imports — the package's `./boot` export; never a sketch specifier. */
|
|
50
|
-
export const FRQTL_WEB_BOOT = Object.freeze({ specifier: '@descix/frqtl-web/boot', libPath: 'frqtl/experiments/_shared/doors.js'
|
|
52
|
+
export const FRQTL_WEB_BOOT = Object.freeze({ specifier: '@descix/frqtl-web/boot', libPath: 'frqtl/experiments/_shared/doors.js',
|
|
53
|
+
why: 'the scene doors (bootScene, bootProject)' });
|
|
54
|
+
|
|
55
|
+
/** What a host page imports beside its sketches. Every row is a public exports entry of @descix/frqtl-web. */
|
|
56
|
+
export const FRQTL_WEB_HOST_SPECIFIERS = Object.freeze([
|
|
57
|
+
FRQTL_WEB_BOOT,
|
|
58
|
+
Object.freeze({ specifier: '@descix/frqtl-web/registry', libPath: 'shim/registry/index.js',
|
|
59
|
+
why: 'the walk-through rungs, the showcase roster, the experiment registry, projectOf' }),
|
|
60
|
+
Object.freeze({ specifier: '@descix/frqtl-web/project', libPath: 'shim/project/index.js',
|
|
61
|
+
why: 'the specifier table, the link site, sketchProject and the project identity' })
|
|
62
|
+
]);
|
|
51
63
|
|
|
52
64
|
/** The package a self-reference names. */
|
|
53
65
|
export const FRQTL_WEB_PACKAGE = '@descix/frqtl-web';
|
|
@@ -80,9 +92,15 @@ export function libPathOf(specifier) {
|
|
|
80
92
|
'. (FAIL-LOUD.)');
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
/** An import map resolves an address only if it is an absolute URL or starts with '/', './' or '../'. */
|
|
96
|
+
function _isMapAddress(url) {
|
|
97
|
+
if (/^(\/|\.\/|\.\.\/)/.test(url)) { return true; }
|
|
98
|
+
try { new URL(url); return true; } catch (e) { return false; }
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
/**
|
|
84
|
-
* @param {string} libRootUrl — the lib-identity root as seen from the page, ending in
|
|
85
|
-
* (e.g. '../../', '/pub/abc123/', 'vendor/frqtl-web/0.6.0/dist/')
|
|
102
|
+
* @param {string} libRootUrl — the lib-identity root as seen from the page, ending in `/`: an absolute
|
|
103
|
+
* URL or one starting with '/', './' or '../' (e.g. '../../', '/pub/abc123/', './vendor/frqtl-web/0.6.0/dist/')
|
|
86
104
|
* @returns {{imports: Object<string, string>}}
|
|
87
105
|
*/
|
|
88
106
|
export function importMapFor(libRootUrl) {
|
|
@@ -90,6 +108,12 @@ export function importMapFor(libRootUrl) {
|
|
|
90
108
|
throw new TypeError('[specifiers] importMapFor(libRootUrl): the lib-identity root URL as seen from the page, ' +
|
|
91
109
|
'ending in "/" (got ' + JSON.stringify(libRootUrl) + '). (FAIL-LOUD.)');
|
|
92
110
|
}
|
|
111
|
+
if (!_isMapAddress(libRootUrl)) {
|
|
112
|
+
throw new TypeError('[specifiers] importMapFor(libRootUrl): ' + JSON.stringify(libRootUrl) + ' is neither an ' +
|
|
113
|
+
'absolute URL nor a path starting with "/", "./" or "../", so a browser reads every row built on it as a ' +
|
|
114
|
+
'bare specifier and ignores it. Write it as ' + JSON.stringify('./' + libRootUrl) + ' (relative to the page) ' +
|
|
115
|
+
'or ' + JSON.stringify('/' + libRootUrl) + ' (from the origin root). (FAIL-LOUD.)');
|
|
116
|
+
}
|
|
93
117
|
const imports = {};
|
|
94
118
|
for (const row of FRQTL_WEB_SPECIFIERS) { imports[row.specifier] = libRootUrl + row.libPath; }
|
|
95
119
|
return { imports: imports };
|
|
@@ -97,10 +121,14 @@ export function importMapFor(libRootUrl) {
|
|
|
97
121
|
|
|
98
122
|
/**
|
|
99
123
|
* @param {string} libRootUrl — as importMapFor's
|
|
100
|
-
* @returns {{imports: Object<string, string>}} importMapFor(libRootUrl) plus
|
|
124
|
+
* @returns {{imports: Object<string, string>}} importMapFor(libRootUrl) plus every FRQTL_WEB_HOST_SPECIFIERS row
|
|
125
|
+
*
|
|
126
|
+
* The map carries no dist-manifest.json row: the manifest is a fact of a PACKAGE dist, and this map is also
|
|
127
|
+
* rendered for lib roots that are not one (the static site's graph has none). A host reads it from the root
|
|
128
|
+
* it already holds: `new URL('dist-manifest.json', root)`.
|
|
101
129
|
*/
|
|
102
130
|
export function pageImportMapFor(libRootUrl) {
|
|
103
131
|
const map = importMapFor(libRootUrl);
|
|
104
|
-
map.imports[
|
|
132
|
+
for (const row of FRQTL_WEB_HOST_SPECIFIERS) { map.imports[row.specifier] = libRootUrl + row.libPath; }
|
|
105
133
|
return map;
|
|
106
134
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* walkthrough.js the walk-through rungs: walkthroughById, effectiveRenderState, modes
|
|
11
11
|
* exhibits.js the showcase roster
|
|
12
12
|
* experiments.js the experiment registry and its tiers
|
|
13
|
-
* scene-source.js
|
|
13
|
+
* scene-source.js projectOf(id, { mode }) — a rung as a project folder + its renderState (fetch in a page, file read in node)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export * from './walkthrough.js';
|
|
@@ -1,38 +1,46 @@
|
|
|
1
1
|
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* scene-source.js — the ONE reader of a scene
|
|
4
|
+
* scene-source.js — the ONE reader of a registry scene as a PROJECT, in a page AND in node.
|
|
5
5
|
*
|
|
6
|
-
* A scene is ONE file, `lib/frqtl/scenes/<file>.js`: a
|
|
7
|
-
*
|
|
8
|
-
* its frqtl-relative PATH (`source: 'scenes/<file>.js'`), never its text. Every consumer
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* A scene is ONE sketch file, `lib/frqtl/scenes/<file>.js`: a module whose default export is
|
|
7
|
+
* `buildExperiment(exp)`, holding the whole scene (world, parts, render intent). A registry row carries
|
|
8
|
+
* its frqtl-relative PATH (`source: 'scenes/<file>.js'`), never its text. Every consumer that needs the
|
|
9
|
+
* scene (the GYM's rung frame, STUDIO, the hero, a package consumer of `@descix/frqtl-web/registry`) asks
|
|
10
|
+
* here, so the id → path → URL → project chain exists once.
|
|
11
11
|
*
|
|
12
|
-
* scenePath(rung)
|
|
13
|
-
*
|
|
12
|
+
* scenePath(rung) → 'scenes/<file>.js' (THROWS on any other shape)
|
|
13
|
+
* projectOf(scene, { router?, fetch?, mode? })
|
|
14
|
+
* → Promise<{ manifest, files, renderState }>
|
|
15
|
+
* files { 'frqtl.json': serializeManifest(manifestFor({ name: row.id, entry: 'sketch.js' })),
|
|
16
|
+
* 'sketch.js': <the scene file's text> } — the folder a host boots
|
|
17
|
+
* (bootProject(mount, { files, mint }, { renderState }), no entry needed)
|
|
18
|
+
* manifest that frqtl.json's normalized bag
|
|
19
|
+
* renderState effectiveRenderState(row, mode) — the registry's one derivation, ferried BESIDE the folder
|
|
20
|
+
* (boot facts never enter frqtl.json or the project identity)
|
|
14
21
|
*
|
|
15
|
-
* `scene` is a rung id (resolved by `walkthroughById`; an unknown id REJECTS naming the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
22
|
+
* `scene` is a rung id (resolved by `walkthroughById`; an unknown id REJECTS naming the declared ids) or a
|
|
23
|
+
* row (`{id, source}`, e.g. `EMPTY_SCENE`). The URL comes from a router, the one path authority
|
|
24
|
+
* (lib/shim/engine/router.js, `scenes/` profile key):
|
|
18
25
|
* - `opts.router` — the host's own (the IDE's engineRouter); a host always passes it.
|
|
19
|
-
* - absent — the PACKAGE's: the dist layout anchored at the dist root this module ships
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* read through node's fs (`process.getBuiltinModule`), anything else is fetched.
|
|
26
|
+
* - absent — the PACKAGE's: the dist layout anchored at the dist root this module ships in
|
|
27
|
+
* (`PACKAGE_ROOT_FROM_HERE`, dist/shim/registry/ → dist/). This is the `@descix/frqtl-web/registry`
|
|
28
|
+
* door's reader; in the source tree it names a file that is not there and rejects.
|
|
29
|
+
* The read is a CAPABILITY split on the resolved URL, never a host sniff: a `file:` URL is read through
|
|
30
|
+
* node's fs (`process.getBuiltinModule`), anything else is fetched.
|
|
25
31
|
*
|
|
26
|
-
* FAIL-LOUD: an unknown id, a row without a scenes/ path, a non-200 response, an
|
|
27
|
-
* file or an empty body REJECTS naming the scene and the URL. A blank scene is never a result.
|
|
32
|
+
* FAIL-LOUD: an unknown id, a row without a scenes/ path, an undeclared mode, a non-200 response, an
|
|
33
|
+
* unreadable file or an empty body REJECTS naming the scene and the URL. A blank scene is never a result.
|
|
28
34
|
*
|
|
29
35
|
* Zero physics: this moves text. The scene's physics is whatever its file composes.
|
|
30
36
|
*/
|
|
31
37
|
|
|
32
38
|
'use strict';
|
|
33
39
|
|
|
34
|
-
import { WALKTHROUGH, walkthroughById } from './walkthrough.js';
|
|
40
|
+
import { WALKTHROUGH, walkthroughById, effectiveRenderState } from './walkthrough.js';
|
|
35
41
|
import { engineRouter } from '../engine/router.js';
|
|
42
|
+
import { MANIFEST_FILE, manifestFor, serializeManifest } from '../project/manifest.js';
|
|
43
|
+
import { SKETCH_ENTRY } from '../project/link.js';
|
|
36
44
|
|
|
37
45
|
/** The one path prefix a scene file lives under (the router's `scenes/` profile key). */
|
|
38
46
|
export var SCENES_PREFIX = 'scenes/';
|
|
@@ -47,11 +55,11 @@ var PACKAGE_ROOT_FROM_HERE = '../../';
|
|
|
47
55
|
|
|
48
56
|
/**
|
|
49
57
|
* The blank world — STUDIO's "Empty Universe" — is itself a scene file (the empty page states no
|
|
50
|
-
* world of its own). A row-shaped handle so it reads through the same `
|
|
58
|
+
* world of its own). A row-shaped handle so it reads through the same `projectOf`.
|
|
51
59
|
*/
|
|
52
60
|
export var EMPTY_SCENE = Object.freeze({ id: 'empty', source: 'scenes/empty.js' });
|
|
53
61
|
|
|
54
|
-
var OPT_KEYS = ['router', 'fetch'];
|
|
62
|
+
var OPT_KEYS = ['router', 'fetch', 'mode'];
|
|
55
63
|
|
|
56
64
|
/**
|
|
57
65
|
* @param {{id?:string, source?:string}} rung — a registry row.
|
|
@@ -77,7 +85,7 @@ function _rowOf(scene) {
|
|
|
77
85
|
return rung;
|
|
78
86
|
}
|
|
79
87
|
if (!scene || typeof scene !== 'object') {
|
|
80
|
-
throw new Error('[scene-source]
|
|
88
|
+
throw new Error('[scene-source] projectOf takes a rung id or a registry row, got ' +
|
|
81
89
|
JSON.stringify(scene) + '. (FAIL-LOUD.)');
|
|
82
90
|
}
|
|
83
91
|
return scene;
|
|
@@ -119,24 +127,26 @@ function _fetchText(url, id, fetchImpl) {
|
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
/**
|
|
122
|
-
*
|
|
130
|
+
* A registry scene as the project folder a host boots, with the host's boot bag beside it.
|
|
123
131
|
* @param {string|{id:string, source:string}} scene — a rung id, or a registry row.
|
|
124
|
-
* @param {{router?:{resolve:function(string):string}, fetch?:function}} [opts]
|
|
132
|
+
* @param {{router?:{resolve:function(string):string}, fetch?:function, mode?:string|null}} [opts]
|
|
125
133
|
* router — the host's engineRouter (absent: the package's own dist root);
|
|
126
|
-
* fetch — the fetch used for a non-file URL (defaults to the global; injectable for tests)
|
|
127
|
-
*
|
|
134
|
+
* fetch — the fetch used for a non-file URL (defaults to the global; injectable for tests);
|
|
135
|
+
* mode — a declared walk-through mode (absent: the rung's default) for the renderState.
|
|
136
|
+
* @returns {Promise<{manifest: object, files: Object<string,string>, renderState: object}>}
|
|
128
137
|
*/
|
|
129
|
-
export function
|
|
138
|
+
export function projectOf(scene, opts) {
|
|
130
139
|
return Promise.resolve().then(function () {
|
|
131
140
|
opts = opts || {};
|
|
132
141
|
Object.keys(opts).forEach(function (k) {
|
|
133
142
|
if (OPT_KEYS.indexOf(k) === -1) {
|
|
134
|
-
throw new Error('[scene-source]
|
|
143
|
+
throw new Error('[scene-source] projectOf has no option "' + k + '" — options: ' +
|
|
135
144
|
OPT_KEYS.join(', ') + '. (FAIL-LOUD.)');
|
|
136
145
|
}
|
|
137
146
|
});
|
|
138
147
|
var row = _rowOf(scene);
|
|
139
148
|
var rel = scenePath(row);
|
|
149
|
+
var renderState = effectiveRenderState(row, opts.mode); // an undeclared mode rejects before the read
|
|
140
150
|
var router = opts.router || _packageRouter();
|
|
141
151
|
if (typeof router.resolve !== 'function') {
|
|
142
152
|
throw new Error('[scene-source] opts.router has no resolve() — pass an engineRouter ' +
|
|
@@ -151,7 +161,11 @@ export function loadSceneSource(scene, opts) {
|
|
|
151
161
|
throw new Error('[scene-source] scene "' + row.id + '" — ' + url +
|
|
152
162
|
' has an empty body. (FAIL-LOUD.)');
|
|
153
163
|
}
|
|
154
|
-
|
|
164
|
+
var manifest = manifestFor({ name: row.id, entry: SKETCH_ENTRY });
|
|
165
|
+
var files = {};
|
|
166
|
+
files[MANIFEST_FILE] = serializeManifest(manifest);
|
|
167
|
+
files[SKETCH_ENTRY] = t;
|
|
168
|
+
return { manifest: manifest, files: files, renderState: renderState };
|
|
155
169
|
});
|
|
156
170
|
});
|
|
157
171
|
}
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*
|
|
12
12
|
* A rung row carries the PATH of its scene, never its text: each scene is ONE sketch
|
|
13
13
|
* file, `lib/frqtl/scenes/<file>.js` — a dual-mode `buildExperiment(exp)` module that
|
|
14
|
-
* holds the whole scene.
|
|
15
|
-
* (./scene-source.js:
|
|
16
|
-
* a non-200 or an empty body). Rungs
|
|
14
|
+
* holds the whole scene. A host reads it as a project through `projectOf(id | rung, opts)`
|
|
15
|
+
* (./scene-source.js: frqtl.json + sketch.js, with effectiveRenderState beside it; a fetch in a
|
|
16
|
+
* page, a file read in node; FAIL-LOUD on an unknown id, a non-200 or an empty body). Rungs
|
|
17
|
+
* that share one physics name the SAME file.
|
|
17
18
|
*
|
|
18
19
|
* ── Rung descriptor ──────────────────────────────────────────────────────────
|
|
19
20
|
* id : stable dropdown id (the `walkthrough:<id>` selection value)
|
|
@@ -46,17 +47,17 @@
|
|
|
46
47
|
* is the config-mirror anti-pattern this contract exists to kill. Every mode delta
|
|
47
48
|
* must be expressible as DECLARED descriptor fields (emitter `is2D`, Wall/Tunnel
|
|
48
49
|
* `z`/`depth`, ordinary Walls for the z-collimators, camera on the renderState) so
|
|
49
|
-
* a built scene
|
|
50
|
+
* a built scene's declared parts hold concrete, mode-PINNED state and survive Restart.
|
|
50
51
|
*
|
|
51
|
-
* ── The recipe form (
|
|
52
|
+
* ── The recipe form (a parts block, written back by the parts writer) ─────────
|
|
52
53
|
* Rungs are OBJECT recipes: each body states the WORLD first, in its co-init call
|
|
53
|
-
* (`exp.createCanvas(
|
|
54
|
-
* `exp.
|
|
55
|
-
* world, the sketch does, once, before any part.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
54
|
+
* (`exp.createCanvas(…)` / `exp.createScene(…)`), then declares its parts —
|
|
55
|
+
* `const p = exp.declare(parts)` over the module's `export const parts = { name: [Kind, props] }`
|
|
56
|
+
* block — a PART never inits the world, the sketch does, once, before any part. Every
|
|
57
|
+
* declared part is a real authored object, so a loaded rung is DRAGGABLE / INSPECTABLE,
|
|
58
|
+
* and a STUDIO hand (grab/drag, Inspector, knob, visibility) is written into the SAME
|
|
59
|
+
* sketch text by the parts writer, `await exp.toSketch()`: it patches the touched
|
|
60
|
+
* entries of the parts block in place and leaves every other line of the file as authored.
|
|
60
61
|
*
|
|
61
62
|
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
62
63
|
* Zero new physics (canonical-bench SU1) — every rung composes already-goldened
|