@descix/frqtl-node 0.6.0-p4.1 → 0.6.0
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 +5 -3
- package/dist/dist-manifest.json +58 -54
- package/dist/fat/pkg-node/package.json +2 -10
- package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
- package/dist/frqtl/objects/FrqtlApp.js +5 -5
- package/dist/frqtl/objects/RecipeContext.js +106 -1
- package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
- package/dist/frqtl/objects/instruments/capture.js +1 -24
- package/dist/frqtl/objects/instruments/hosted-eyes.js +17 -1
- package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
- package/dist/frqtl/objects/record/Record.js +25 -5
- package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
- package/dist/frqtl/objects/schema/scopes.js +2 -2
- package/dist/frqtl/objects/setup/_worldInit.js +1 -2
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
- package/dist/frqtl/parts/CameraControls.js +8 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
- package/dist/frqtl/parts/LargeObject.js +2 -2
- package/dist/frqtl/parts/LiveChart.js +2 -2
- package/dist/frqtl/parts/LogicGate.js +3 -3
- package/dist/frqtl/parts/OvenBox.js +1 -1
- package/dist/frqtl/parts/PointSource.js +1 -1
- package/dist/frqtl/parts/SlitLightSource.js +1 -1
- package/dist/frqtl/parts/SlitScreen.js +1 -1
- package/dist/frqtl/parts/SphereEmitter.js +12 -12
- package/dist/frqtl/parts/Trace.js +1 -1
- package/dist/frqtl/parts/Tunnel.js +11 -11
- package/dist/frqtl/parts/Wall.js +20 -10
- package/dist/frqtl/scenes/circuit-sat.js +1 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
- package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
- package/dist/frqtl/scenes/mirror-route.js +96 -79
- package/dist/shim/bundle/index.js +6 -0
- package/dist/shim/controls/index.js +1 -1
- package/dist/shim/engine/artifact-shell.js +1 -1
- package/dist/shim/engine/router.js +3 -3
- package/dist/shim/host/host-view.js +228 -0
- package/dist/shim/host/index.js +18 -0
- package/dist/shim/index.js +2 -4
- package/dist/shim/notebook/kernel.js +1 -1
- package/dist/shim/notebook/setup-builder.js +2 -2
- package/dist/shim/package.json +1 -0
- package/dist/shim/page-params/index.js +2 -30
- package/dist/shim/project/fork.js +293 -0
- package/dist/shim/project/identity.js +3 -1
- package/dist/shim/project/link.js +43 -3
- package/dist/shim/project/parts-writer.js +62 -5
- package/dist/shim/project/read-text.js +43 -0
- package/dist/shim/project/specifiers.js +104 -15
- package/dist/shim/registry/exhibits.js +15 -34
- package/dist/shim/registry/index.js +2 -2
- package/dist/shim/registry/scene-roster.js +35 -0
- package/dist/shim/registry/scene-source.js +6 -36
- package/dist/shim/registry/walkthrough.js +2 -3
- package/dist/shim/render-state/apply.js +66 -31
- package/dist/shim/render-state/index.js +6 -2
- package/dist/shim/runtime/index.js +4 -4
- package/llms.txt +4 -3
- package/package.json +19 -1
- package/dist/shim/registry/experiments.js +0 -215
|
@@ -103,7 +103,9 @@ export async function projectIdentity(project) {
|
|
|
103
103
|
for (const c of chunks) { stream.set(c, at); at += c.length; }
|
|
104
104
|
const subtle = (typeof crypto !== 'undefined') ? crypto.subtle : null;
|
|
105
105
|
if (!subtle || typeof subtle.digest !== 'function') {
|
|
106
|
-
_fail('crypto.subtle
|
|
106
|
+
_fail('crypto.subtle is unavailable — this page is not a secure context (isSecureContext ' + String(typeof isSecureContext === 'undefined' ? undefined : isSecureContext) +
|
|
107
|
+
'): serve the folder from https or from localhost (e.g. npx serve). The project identity is a content hash with no second ' +
|
|
108
|
+
'hashing path.');
|
|
107
109
|
}
|
|
108
110
|
const view = new Uint8Array(await subtle.digest('SHA-256', stream));
|
|
109
111
|
let hex = '';
|
|
@@ -47,11 +47,15 @@
|
|
|
47
47
|
* is minted, each naming the file, the specifier and the reason: a relative specifier that climbs out of
|
|
48
48
|
* the root (package internals have no public specifier), one that names a non-module path (not ".js" /
|
|
49
49
|
* ".mjs": a data file, an extensionless name, frqtl.json), one that names no project file (never an HTTP
|
|
50
|
-
* sibling), a @descix/frqtl-web specifier that is not a row of FRQTL_WEB_SPECIFIERS,
|
|
50
|
+
* sibling), a @descix/frqtl-web specifier that is not a row of FRQTL_WEB_SPECIFIERS, a part-internal row imported
|
|
51
|
+
* by any file but a part file (parts/*.js), any other specifier;
|
|
51
52
|
* an import cycle among project files; a module file the entry never reaches; an invalid frqtl.json; a
|
|
52
53
|
* served file that is not UTF-8; a missing or non-function default export of the entry, a missing entry,
|
|
53
54
|
* an empty project. Every argument error REJECTS.
|
|
54
55
|
*
|
|
56
|
+
* An error raised while the project EVALUATES (a duplicate kind, a throw at module top) is rethrown with every minted
|
|
57
|
+
* URL in its message named by its project path and every package file URL by its specifier (_namePaths).
|
|
58
|
+
*
|
|
55
59
|
* blobMint(text) → { url, release() } a `blob:` module URL (a page), one per byte-identical text
|
|
56
60
|
* dataMint(text) → { url, release() } a `data:` module URL (node)
|
|
57
61
|
*
|
|
@@ -59,7 +63,7 @@
|
|
|
59
63
|
* sketchProject(text) is that shape's one writer inside the tree (SKETCH_ENTRY its entry path).
|
|
60
64
|
*/
|
|
61
65
|
|
|
62
|
-
import { libPathOf, FRQTL_WEB_PACKAGE, FRQTL_WEB_SPECIFIERS } from './specifiers.js';
|
|
66
|
+
import { libPathOf, tierOf, specifierOfLibPath, FRQTL_WEB_PACKAGE, FRQTL_WEB_SPECIFIERS } from './specifiers.js';
|
|
63
67
|
import { checkProjectPath, projectBytes } from './identity.js';
|
|
64
68
|
import { MANIFEST_FILE, parseManifest } from './manifest.js';
|
|
65
69
|
import { packageRoot } from '../../frqtl/compat/package-root.js';
|
|
@@ -134,6 +138,14 @@ function _publishedList() {
|
|
|
134
138
|
return FRQTL_WEB_SPECIFIERS.map(function (r) { return r.specifier + (r.specifier.endsWith('/') ? '<file>' : ''); }).join(', ');
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
/** The tier a forked part's internals carry (specifiers.js SPECIFIER_TIERS). */
|
|
142
|
+
const PART_INTERNAL = 'part-internal';
|
|
143
|
+
|
|
144
|
+
/** A project file under parts/ (a part file: the only importer of a part-internal module). */
|
|
145
|
+
function _isPartFile(p) {
|
|
146
|
+
return /^parts\/[^/]+\.m?js$/.test(p);
|
|
147
|
+
}
|
|
148
|
+
|
|
137
149
|
function _isPackage(spec) {
|
|
138
150
|
return spec === FRQTL_WEB_PACKAGE || spec.startsWith(FRQTL_WEB_PACKAGE + '/');
|
|
139
151
|
}
|
|
@@ -198,6 +210,10 @@ async function _walk(entry, sourceOf) {
|
|
|
198
210
|
_fail('"' + p + '" imports "' + imp.spec + '" — not a published specifier; the table ' +
|
|
199
211
|
'(lib/shim/project/specifiers.js FRQTL_WEB_SPECIFIERS) offers: ' + _publishedList() + '.');
|
|
200
212
|
}
|
|
213
|
+
if (tierOf(imp.spec) === PART_INTERNAL && !_isPartFile(p)) {
|
|
214
|
+
_fail('"' + p + '" imports "' + imp.spec + '" — a part-internal module is imported by a part file ' +
|
|
215
|
+
'(parts/*.js), never by a sketch — a sketch imports parts by @descix/frqtl-web/parts.');
|
|
216
|
+
}
|
|
201
217
|
list.push(Object.assign({ kind: 'package' }, imp));
|
|
202
218
|
} else {
|
|
203
219
|
_fail('"' + p + '" imports "' + imp.spec + '" — a project file imports another project file by a ' +
|
|
@@ -463,7 +479,9 @@ export async function linkProject(project) {
|
|
|
463
479
|
minted.push(m);
|
|
464
480
|
urls[p] = m.url;
|
|
465
481
|
}
|
|
466
|
-
|
|
482
|
+
let ns;
|
|
483
|
+
try { ns = await import(urls[entry]); }
|
|
484
|
+
catch (err) { throw _namePaths(err, urls); }
|
|
467
485
|
const held = { files: heldFiles, entry: entry, mint: mint };
|
|
468
486
|
if (project.bare !== undefined) { held.bare = project.bare; }
|
|
469
487
|
return { buildExperiment: _entryFunction(ns, entry), module: ns, manifest: manifest, files: bytes,
|
|
@@ -473,6 +491,28 @@ export async function linkProject(project) {
|
|
|
473
491
|
}
|
|
474
492
|
}
|
|
475
493
|
|
|
494
|
+
/**
|
|
495
|
+
* An error raised while the project evaluates, with every minted URL in its message replaced by the project path
|
|
496
|
+
* it names (byte-identical files share one minted URL: all their paths), and every package file URL by its
|
|
497
|
+
* specifier — so a duplicate kind reads "kind "Wall" is already registered by @descix/frqtl-web/parts/Wall.js;
|
|
498
|
+
* parts/Wall2.js registers it again". The SAME error is rethrown (its stack kept): enrichment, never a catch.
|
|
499
|
+
*/
|
|
500
|
+
function _namePaths(err, urls) {
|
|
501
|
+
if (!err || typeof err.message !== 'string') { return err; }
|
|
502
|
+
const byUrl = new Map();
|
|
503
|
+
for (const p of Object.keys(urls)) { byUrl.set(urls[p], (byUrl.get(urls[p]) || []).concat(p)); }
|
|
504
|
+
let msg = err.message;
|
|
505
|
+
for (const [url, paths] of [...byUrl.entries()].sort(function (a, b) { return b[0].length - a[0].length; })) {
|
|
506
|
+
msg = msg.split(url).join(paths.join(' (= ') + (paths.length > 1 ? ', byte-identical)' : ''));
|
|
507
|
+
}
|
|
508
|
+
const root = packageRoot();
|
|
509
|
+
msg = msg.replace(new RegExp(root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '([^\\s;,)\'"]+)', 'g'), function (m, lp) {
|
|
510
|
+
return specifierOfLibPath(lp) || m;
|
|
511
|
+
});
|
|
512
|
+
try { err.message = msg; } catch (_) { /* a frozen error keeps its own text */ }
|
|
513
|
+
return err;
|
|
514
|
+
}
|
|
515
|
+
|
|
476
516
|
/** The entry path of a one-string sketch held as a project. */
|
|
477
517
|
export const SKETCH_ENTRY = 'sketch.js';
|
|
478
518
|
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* import line for Kind (after the last import statement;
|
|
17
17
|
* `module` from parts-module.js moduleOfKind)
|
|
18
18
|
* | { op: 'remove', name } the entry's property node, its comma included
|
|
19
|
+
* | { op: 'kind', name, Kind, module } within entry `name`, replace ONLY its Kind identifier's text, plus
|
|
20
|
+
* at most one import line for Kind (the add's line, after the last
|
|
21
|
+
* import); the entry's old import line is the author's and stays,
|
|
22
|
+
* even when nothing uses it now (a fork: exp.fork)
|
|
19
23
|
*
|
|
20
24
|
* changes[i] = { op, name, key?, from?, to, literalized? } from/to are source TEXT; `literalized: true`
|
|
21
25
|
* when the old value was an expression (its text is `from`), now its literal value
|
|
@@ -29,7 +33,8 @@
|
|
|
29
33
|
*
|
|
30
34
|
* entryKeys(text, name) → string[] the plain prop keys entry `name` spells (how an entry spells a value)
|
|
31
35
|
* entryLiteral(text, name, key) → { literal, value?, length? } what a key declares when it is a literal
|
|
32
|
-
* (read-back); an array expression's element count otherwise
|
|
36
|
+
* (read-back); an array expression's element count otherwise (none when
|
|
37
|
+
* an element is a spread)
|
|
33
38
|
*
|
|
34
39
|
* serializeValue(value, key) → text THE one value serializer: numbers, strings (single-quoted),
|
|
35
40
|
* booleans, null, arrays inline, plain objects. Anything else THROWS
|
|
@@ -39,6 +44,14 @@
|
|
|
39
44
|
import { parse } from '../../frqtl/assets/vendor/acorn.mjs';
|
|
40
45
|
import { importSpecifierOf } from './parts-module.js';
|
|
41
46
|
|
|
47
|
+
/**
|
|
48
|
+
* parseModule(text) → the acorn syntax tree of an ES module (latest syntax, source ranges) — the vendored parser's
|
|
49
|
+
* ONE entry point in the tree; every other reader of a project file's syntax (fork.js) parses through it.
|
|
50
|
+
*/
|
|
51
|
+
export function parseModule(text) {
|
|
52
|
+
return parse(text, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
/** The export the writer patches. */
|
|
43
56
|
export const PARTS_EXPORT = 'parts';
|
|
44
57
|
|
|
@@ -47,6 +60,14 @@ const _RESERVED = new Set(('break case catch class const continue debugger defau
|
|
|
47
60
|
'false finally for function if import in instanceof new null return super switch this throw true try typeof ' +
|
|
48
61
|
'var void while with yield let static enum await implements package protected interface private public').split(' '));
|
|
49
62
|
|
|
63
|
+
/**
|
|
64
|
+
* isIdentifierName(s) → true when `s` can name a binding (an identifier that is not a reserved word) — the one
|
|
65
|
+
* test the writer and the fork transform share.
|
|
66
|
+
*/
|
|
67
|
+
export function isIdentifierName(s) {
|
|
68
|
+
return typeof s === 'string' && _IDENT.test(s) && !_RESERVED.has(s);
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
function _keyText(k) {
|
|
51
72
|
return (_IDENT.test(k) && !_RESERVED.has(k)) ? k : _quote(k);
|
|
52
73
|
}
|
|
@@ -230,7 +251,7 @@ export function patchParts(text, edits, opts) {
|
|
|
230
251
|
throw new TypeError('[parts-writer] patchParts(text, edits): text is the sketch file (got ' + typeof text + '). (FAIL-LOUD.)');
|
|
231
252
|
}
|
|
232
253
|
if (!Array.isArray(edits)) {
|
|
233
|
-
throw new TypeError('[parts-writer] patchParts(text, edits): edits is an array of { op: set | add | remove }. (FAIL-LOUD.)');
|
|
254
|
+
throw new TypeError('[parts-writer] patchParts(text, edits): edits is an array of { op: set | add | remove | kind }. (FAIL-LOUD.)');
|
|
234
255
|
}
|
|
235
256
|
const changes = [];
|
|
236
257
|
const refused = [];
|
|
@@ -317,6 +338,37 @@ export function patchParts(text, edits, opts) {
|
|
|
317
338
|
const ins = _appendProperty(text, obj, appended.join(_multiline(text, obj) ? ',\n' + _indentAt(text, (obj.properties[obj.properties.length - 1] || obj).start) : ', '));
|
|
318
339
|
patches.push(ins);
|
|
319
340
|
}
|
|
341
|
+
} else if (e.op === 'kind') {
|
|
342
|
+
const p = entryOf(e);
|
|
343
|
+
if (!p) { continue; }
|
|
344
|
+
const v = p.value;
|
|
345
|
+
if (v.type !== 'ArrayExpression' || v.elements.length !== 2 || !v.elements[0] || v.elements[0].type !== 'Identifier') {
|
|
346
|
+
refuse(e, 'the entry "' + e.name + '" is not written as [Kind, { …props }] — the writer patches only that form.');
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (typeof e.Kind !== 'string' || !_IDENT.test(e.Kind)) {
|
|
350
|
+
throw new TypeError('[parts-writer] kind: Kind is the new kind\'s class name (got ' + JSON.stringify(e.Kind) + '). (FAIL-LOUD.)');
|
|
351
|
+
}
|
|
352
|
+
if (e.module === null || e.module === undefined) {
|
|
353
|
+
refuse(e, 'the kind ' + e.Kind + ' has no importable module, so a sketch cannot import it.');
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const spec = importSpecifierOf(e.module, entry);
|
|
357
|
+
const b = bindings.get(e.Kind);
|
|
358
|
+
if (b && !(b.kind === 'import' && b.source === spec && b.imported === e.Kind)) {
|
|
359
|
+
refuse(e, 'the identifier ' + e.Kind + ' is already bound in this sketch to ' +
|
|
360
|
+
(b.kind === 'import' ? '"' + (b.imported || '*') + '" from \'' + b.source + '\'' : 'its own declaration') +
|
|
361
|
+
' — the writer cannot import the kind ' + e.Kind + ' under that name.');
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const id = v.elements[0];
|
|
365
|
+
if (id.name === e.Kind) { continue; }
|
|
366
|
+
patches.push({ start: id.start, end: id.end, text: e.Kind });
|
|
367
|
+
const line = 'import { ' + e.Kind + ' } from ' + _quote(spec) + ';';
|
|
368
|
+
if (!b && !importLines.some(function (l) { return l.Kind === e.Kind; })) {
|
|
369
|
+
importLines.push({ Kind: e.Kind, line: line });
|
|
370
|
+
}
|
|
371
|
+
changes.push({ op: 'kind', name: e.name, from: id.name, to: e.Kind, import: (!b ? line : null) });
|
|
320
372
|
} else if (e.op === 'remove') {
|
|
321
373
|
const p = entryOf(e);
|
|
322
374
|
if (!p) { continue; }
|
|
@@ -353,7 +405,7 @@ export function patchParts(text, edits, opts) {
|
|
|
353
405
|
changes.push({ op: 'add', name: e.name, from: null, to: _keyText(e.name) + ': [' + e.Kind + ', ' + lit + ']',
|
|
354
406
|
import: (!b ? 'import { ' + e.Kind + ' } from ' + _quote(spec) + ';' : null) });
|
|
355
407
|
} else {
|
|
356
|
-
throw new TypeError('[parts-writer] unknown edit op ' + JSON.stringify(e.op) + ' — set | add | remove. (FAIL-LOUD.)');
|
|
408
|
+
throw new TypeError('[parts-writer] unknown edit op ' + JSON.stringify(e.op) + ' — set | add | remove | kind. (FAIL-LOUD.)');
|
|
357
409
|
}
|
|
358
410
|
}
|
|
359
411
|
if (adds.length) {
|
|
@@ -450,8 +502,13 @@ export function entryLiteral(text, name, key) {
|
|
|
450
502
|
for (const q of _entryProps(text, name, 'entryLiteral').properties) {
|
|
451
503
|
if (_plainKey(q) !== key) { continue; }
|
|
452
504
|
if (_isLiteral(q.value)) { return { literal: true, value: _literalValue(q.value) }; }
|
|
453
|
-
// An array expression still spells its arity (`center: [srcX, cy]` is two numbers)
|
|
454
|
-
|
|
505
|
+
// An array expression still spells its arity (`center: [srcX, cy]` is two numbers) — unless an element is a
|
|
506
|
+
// spread (`center: [...c]`), which spells no arity: the caller applies the live one.
|
|
507
|
+
const arr = q.value.type === 'ArrayExpression';
|
|
508
|
+
if (arr && !q.value.elements.some(function (x) { return x && x.type === 'SpreadElement'; })) {
|
|
509
|
+
return { literal: false, length: q.value.elements.length };
|
|
510
|
+
}
|
|
511
|
+
return { literal: false };
|
|
455
512
|
}
|
|
456
513
|
throw new Error('[parts-writer] entryLiteral: the entry "' + name + '" spells no key "' + key + '". (FAIL-LOUD.)');
|
|
457
514
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* read-text.js — THE reader of a text file by URL, for every host: a `file:` URL is read through node's fs
|
|
5
|
+
* (`process.getBuiltinModule`, Node 20.16+ / 22.3+); any other URL is fetched. The split is a CAPABILITY of the
|
|
6
|
+
* resolved URL, never a host sniff.
|
|
7
|
+
*
|
|
8
|
+
* readText(url[, { fetch }]) → Promise<string> REJECTS naming the URL: a file it cannot read ("reading <url>
|
|
9
|
+
* failed: …"), a non-200 ("GET <url> answered <status>"), no
|
|
10
|
+
* fs for a file: URL, no fetch. `fetch` is injectable (tests).
|
|
11
|
+
*
|
|
12
|
+
* Callers prefix their own subject: lib/shim/registry/scene-source.js (a registry scene's sketch) and
|
|
13
|
+
* RecipeContext.fork (a package part's exact bytes).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} url
|
|
18
|
+
* @param {{fetch?: function}} [opts]
|
|
19
|
+
* @returns {Promise<string>}
|
|
20
|
+
*/
|
|
21
|
+
export function readText(url, opts) {
|
|
22
|
+
return Promise.resolve().then(function () {
|
|
23
|
+
if (typeof url !== 'string' || !url) { throw new TypeError('readText(url): a URL string (got ' + JSON.stringify(url) + ')'); }
|
|
24
|
+
if (url.indexOf('file:') === 0) {
|
|
25
|
+
var getBuiltin = (typeof process !== 'undefined' && process.getBuiltinModule) ? process.getBuiltinModule : null;
|
|
26
|
+
if (typeof getBuiltin !== 'function') {
|
|
27
|
+
throw new Error(url + ' is a file: URL; reading one needs node\'s process.getBuiltinModule (Node 20.16+ / ' +
|
|
28
|
+
'22.3+). In a page, serve the package over http');
|
|
29
|
+
}
|
|
30
|
+
var fs = getBuiltin('node:fs');
|
|
31
|
+
var fileURLToPath = getBuiltin('node:url').fileURLToPath;
|
|
32
|
+
return fs.promises.readFile(fileURLToPath(url), 'utf8').catch(function (e) {
|
|
33
|
+
throw new Error('reading ' + url + ' failed: ' + (e && e.message));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
var f = (opts && opts.fetch) || (typeof fetch === 'function' ? fetch : null);
|
|
37
|
+
if (!f) { throw new Error('no fetch available to load ' + url); }
|
|
38
|
+
return Promise.resolve(f(url)).then(function (res) {
|
|
39
|
+
if (!res || !res.ok) { throw new Error('GET ' + url + ' answered ' + (res ? res.status : 'nothing')); }
|
|
40
|
+
return res.text();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* specifiers.js — THE one table of `@descix/frqtl-web/...` specifiers a sketch may import.
|
|
5
5
|
*
|
|
6
|
-
* FRQTL_WEB_SPECIFIERS rows { specifier, libPath, why } — a row whose specifier ends in `/`
|
|
7
|
-
* is a PREFIX row (one part = one file under it)
|
|
6
|
+
* FRQTL_WEB_SPECIFIERS rows { specifier, libPath, why, tier } — a row whose specifier ends in `/`
|
|
7
|
+
* is a PREFIX row (one part = one file under it). `tier` is a key of SPECIFIER_TIERS:
|
|
8
|
+
* 'scene' (a sketch imports it) or 'part-internal' (a forked part file under parts/
|
|
9
|
+
* imports it, never a sketch: the parts' own internal imports, one row per file —
|
|
10
|
+
* the set equals what the registering parts import, held by fork.test.mjs G-CENSUS)
|
|
11
|
+
* SPECIFIER_TIERS { scene, 'part-internal' } — the tier vocabulary and who may import each
|
|
8
12
|
* libPathOf(specifier) → the lib-identity path it names; THROWS on an unknown specifier
|
|
13
|
+
* tierOf(specifier) → the row's tier; THROWS on an unknown specifier (libPathOf's rows)
|
|
14
|
+
* specifierOfLibPath(libPath) → the specifier naming that lib-identity file, or null: libPathOf's inverse over
|
|
15
|
+
* the same rows (a prefix row answers for each file directly under it)
|
|
9
16
|
* importMapFor(libRootUrl) → { imports } — the rows as an import map, every libPath appended to
|
|
10
17
|
* the URL of the lib-identity root AS SEEN FROM THE PAGE: an absolute
|
|
11
18
|
* URL or one starting with '/', './' or '../' (any other root THROWS —
|
|
@@ -20,32 +27,72 @@
|
|
|
20
27
|
*
|
|
21
28
|
* Inside lib/ the specifier is a SELF-REFERENCE — the package naming itself, the way node resolves a
|
|
22
29
|
* package importing itself by name — not a lib→sdk edge. It is legal ONLY in sketch content
|
|
23
|
-
* (lib/frqtl/scenes/**,
|
|
30
|
+
* (lib/frqtl/scenes/**, written sketches, sketch and notebook text); closure modules import relatively
|
|
24
31
|
* (scripts/verify-arrows.mjs holds the scope). Consumers ferry this table and never restate it:
|
|
25
32
|
* every page import map (importMapFor), the node resolve hook (node-specifier-hook.mjs) and the sdk
|
|
26
33
|
* build's module closure (libPathOf).
|
|
27
34
|
*/
|
|
28
35
|
|
|
29
|
-
/**
|
|
36
|
+
/** Who imports a row of each tier. */
|
|
37
|
+
export const SPECIFIER_TIERS = Object.freeze({
|
|
38
|
+
scene: Object.freeze({ id: 'scene', importedBy: 'any project file (a sketch imports it)' }),
|
|
39
|
+
'part-internal': Object.freeze({ id: 'part-internal', importedBy: 'a part file (parts/*.js) of a project — a forked part imports its parent part\'s internals; never a sketch' })
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** The published surface a project imports. Every row is a public exports entry of both packages (sdk/_build/exports-contract.mjs derives them from this table). */
|
|
30
43
|
export const FRQTL_WEB_SPECIFIERS = Object.freeze([
|
|
31
44
|
Object.freeze({ specifier: '@descix/frqtl-web/parts', libPath: 'frqtl/parts/index.js',
|
|
32
|
-
why: 'the parts barrel' }),
|
|
45
|
+
why: 'the parts barrel', tier: 'scene' }),
|
|
33
46
|
Object.freeze({ specifier: '@descix/frqtl-web/parts/', libPath: 'frqtl/parts/',
|
|
34
|
-
why: 'one part = one file' }),
|
|
47
|
+
why: 'one part = one file', tier: 'scene' }),
|
|
35
48
|
Object.freeze({ specifier: '@descix/frqtl-web/shim/controls/registry.js', libPath: 'shim/controls/registry.js',
|
|
36
|
-
why: 'knob registration (blackbody, circuit-sat, collision-matrix-3d, dse-tunnel-rebuild)' }),
|
|
49
|
+
why: 'knob registration (blackbody, circuit-sat, collision-matrix-3d, dse-tunnel-rebuild)', tier: 'scene' }),
|
|
37
50
|
Object.freeze({ specifier: '@descix/frqtl-web/shim/render/sun-look.js', libPath: 'shim/render/sun-look.js',
|
|
38
|
-
why: "the one owner of the sun's FX bag (dse-tunnel-rebuild)" }),
|
|
51
|
+
why: "the one owner of the sun's FX bag (dse-tunnel-rebuild)", tier: 'scene' }),
|
|
39
52
|
Object.freeze({ specifier: '@descix/frqtl-web/compat/renderer/display-slot.js', libPath: 'frqtl/compat/renderer/display-slot.js',
|
|
40
|
-
why: 'the booted display name before co-init (mirror-route, dse-tunnel-rebuild)' }),
|
|
53
|
+
why: 'the booted display name before co-init (mirror-route, dse-tunnel-rebuild)', tier: 'scene' }),
|
|
41
54
|
Object.freeze({ specifier: '@descix/frqtl-web/compat/renderer/orbit-bag.js', libPath: 'frqtl/compat/renderer/orbit-bag.js',
|
|
42
|
-
why: 'hasAngularPose, THE 3D-ness predicate (atomic-model)' }),
|
|
55
|
+
why: 'hasAngularPose, THE 3D-ness predicate (atomic-model)', tier: 'scene' }),
|
|
43
56
|
Object.freeze({ specifier: '@descix/frqtl-web/objects/setup/_collisionMatrixLanes.js', libPath: 'frqtl/objects/setup/_collisionMatrixLanes.js',
|
|
44
|
-
why: 'the one lane table (collision-matrix-3d)' }),
|
|
57
|
+
why: 'the one lane table (collision-matrix-3d)', tier: 'scene' }),
|
|
45
58
|
Object.freeze({ specifier: '@descix/frqtl-web/compat/engine.js', libPath: 'frqtl/compat/engine.js',
|
|
46
|
-
why: 'the engine accessor (atomic-model builds its frames by hand)' }),
|
|
59
|
+
why: 'the engine accessor (atomic-model builds its frames by hand)', tier: 'scene' }),
|
|
47
60
|
Object.freeze({ specifier: '@descix/frqtl-web/kinds', libPath: 'frqtl/objects/kinds-entry.js',
|
|
48
|
-
why: 'the kind registry: a composed fork registers its renamed class (registerKind, kindNamed, kindOf, registeredKinds)' })
|
|
61
|
+
why: 'the kind registry: a composed fork registers its renamed class (registerKind, kindNamed, kindOf, registeredKinds)', tier: 'scene' }),
|
|
62
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/tagger.js', libPath: 'frqtl/objects/objects/tagger.js',
|
|
63
|
+
why: 'imported by DetectorWall — a forked copy imports it', tier: 'part-internal' }),
|
|
64
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/schema/index.js', libPath: 'frqtl/objects/schema/index.js',
|
|
65
|
+
why: 'imported by DetectorWall, SlitScreen, Wall — a forked copy imports it', tier: 'part-internal' }),
|
|
66
|
+
Object.freeze({ specifier: '@descix/frqtl-web/shim/render/heat-colormap.js', libPath: 'shim/render/heat-colormap.js',
|
|
67
|
+
why: 'imported by GraphingDetectorWall — a forked copy imports it', tier: 'part-internal' }),
|
|
68
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/schema/valueNormalizer.js', libPath: 'frqtl/objects/schema/valueNormalizer.js',
|
|
69
|
+
why: 'imported by LiveChart, Wall — a forked copy imports it', tier: 'part-internal' }),
|
|
70
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/Box.js', libPath: 'frqtl/objects/objects/Box.js',
|
|
71
|
+
why: 'imported by OvenBox — a forked copy imports it', tier: 'part-internal' }),
|
|
72
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/QuantumEmitter.js', libPath: 'frqtl/objects/objects/QuantumEmitter.js',
|
|
73
|
+
why: 'imported by OvenBox — a forked copy imports it', tier: 'part-internal' }),
|
|
74
|
+
Object.freeze({ specifier: '@descix/frqtl-web/shim/expr/ast-schema.js', libPath: 'shim/expr/ast-schema.js',
|
|
75
|
+
why: 'imported by OvenBox, Tunnel — a forked copy imports it', tier: 'part-internal' }),
|
|
76
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/actionCompiler.js', libPath: 'frqtl/objects/objects/actionCompiler.js',
|
|
77
|
+
why: 'imported by OvenBox, Tunnel — a forked copy imports it', tier: 'part-internal' }),
|
|
78
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/v1-replacements.js', libPath: 'frqtl/objects/v1-replacements.js',
|
|
79
|
+
why: 'imported by SlitLightSource — a forked copy imports it', tier: 'part-internal' }),
|
|
80
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/slitLinerScatter.js', libPath: 'frqtl/objects/objects/slitLinerScatter.js',
|
|
81
|
+
why: 'imported by SlitScreen, Tunnel — a forked copy imports it', tier: 'part-internal' }),
|
|
82
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/setup/setupSphereEmitter3D.js', libPath: 'frqtl/objects/setup/setupSphereEmitter3D.js',
|
|
83
|
+
why: 'imported by SphereEmitter — a forked copy imports it', tier: 'part-internal' }),
|
|
84
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/directedRedirectProgram.js', libPath: 'frqtl/objects/objects/directedRedirectProgram.js',
|
|
85
|
+
why: 'imported by Tunnel — a forked copy imports it', tier: 'part-internal' }),
|
|
86
|
+
Object.freeze({ specifier: '@descix/frqtl-web/compat/renderer/lo-drawn-center.js', libPath: 'frqtl/compat/renderer/lo-drawn-center.js',
|
|
87
|
+
why: 'imported by Tunnel — a forked copy imports it', tier: 'part-internal' }),
|
|
88
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/setup/setupTunnelPointExit.js', libPath: 'frqtl/objects/setup/setupTunnelPointExit.js',
|
|
89
|
+
why: 'imported by Tunnel — a forked copy imports it', tier: 'part-internal' }),
|
|
90
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/constants/WallBehaviors.js', libPath: 'frqtl/objects/constants/WallBehaviors.js',
|
|
91
|
+
why: 'imported by Wall — a forked copy imports it', tier: 'part-internal' }),
|
|
92
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/absorbRadiation.js', libPath: 'frqtl/objects/objects/absorbRadiation.js',
|
|
93
|
+
why: 'imported by Wall — a forked copy imports it', tier: 'part-internal' }),
|
|
94
|
+
Object.freeze({ specifier: '@descix/frqtl-web/objects/geometry/SlabFace.js', libPath: 'frqtl/objects/geometry/SlabFace.js',
|
|
95
|
+
why: 'imported by Wall — a forked copy imports it', tier: 'part-internal' })
|
|
49
96
|
]);
|
|
50
97
|
|
|
51
98
|
/** The scene doors a host page imports — the package's `./boot` export; never a sketch specifier. */
|
|
@@ -56,9 +103,11 @@ export const FRQTL_WEB_BOOT = Object.freeze({ specifier: '@descix/frqtl-web/boot
|
|
|
56
103
|
export const FRQTL_WEB_HOST_SPECIFIERS = Object.freeze([
|
|
57
104
|
FRQTL_WEB_BOOT,
|
|
58
105
|
Object.freeze({ specifier: '@descix/frqtl-web/registry', libPath: 'shim/registry/index.js',
|
|
59
|
-
why: 'the walk-through rungs, the showcase roster, the
|
|
106
|
+
why: 'the walk-through rungs, the showcase roster, the scene list (sceneGroups), projectOf' }),
|
|
60
107
|
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' })
|
|
108
|
+
why: 'the specifier table, the link site, sketchProject and the project identity' }),
|
|
109
|
+
Object.freeze({ specifier: '@descix/frqtl-web/host-view', libPath: 'shim/host/index.js',
|
|
110
|
+
why: 'the DeSciX host-view declarer: a page declares its own view' })
|
|
62
111
|
]);
|
|
63
112
|
|
|
64
113
|
/** The package a self-reference names. */
|
|
@@ -92,6 +141,46 @@ export function libPathOf(specifier) {
|
|
|
92
141
|
'. (FAIL-LOUD.)');
|
|
93
142
|
}
|
|
94
143
|
|
|
144
|
+
/** The row naming `specifier` (libPathOf's rule: an exact row first, else a prefix row with a file under it), or null. */
|
|
145
|
+
function _rowOf(specifier) {
|
|
146
|
+
for (const row of FRQTL_WEB_SPECIFIERS) {
|
|
147
|
+
if (!_isPrefix(row) && row.specifier === specifier) { return row; }
|
|
148
|
+
}
|
|
149
|
+
for (const row of FRQTL_WEB_SPECIFIERS) {
|
|
150
|
+
if (_isPrefix(row) && specifier.startsWith(row.specifier) && specifier.length > row.specifier.length) { return row; }
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} specifier — a `@descix/frqtl-web/...` specifier
|
|
157
|
+
* @returns {string} its row's tier (a key of SPECIFIER_TIERS); THROWS on a specifier no row names
|
|
158
|
+
*/
|
|
159
|
+
export function tierOf(specifier) {
|
|
160
|
+
libPathOf(specifier); // the one validation (and its refusal text)
|
|
161
|
+
return _rowOf(specifier).tier;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} libPath — a lib-identity path (e.g. 'frqtl/parts/Wall.js', 'frqtl/objects/schema/index.js')
|
|
166
|
+
* @returns {string|null} the specifier whose row names that file, or null when no row does
|
|
167
|
+
*/
|
|
168
|
+
export function specifierOfLibPath(libPath) {
|
|
169
|
+
if (typeof libPath !== 'string' || !libPath) {
|
|
170
|
+
throw new TypeError('[specifiers] specifierOfLibPath(libPath): a lib-identity path (got ' + JSON.stringify(libPath) + '). (FAIL-LOUD.)');
|
|
171
|
+
}
|
|
172
|
+
for (const row of FRQTL_WEB_SPECIFIERS) {
|
|
173
|
+
if (!_isPrefix(row) && row.libPath === libPath) { return row.specifier; }
|
|
174
|
+
}
|
|
175
|
+
for (const row of FRQTL_WEB_SPECIFIERS) {
|
|
176
|
+
if (_isPrefix(row) && libPath.startsWith(row.libPath)) {
|
|
177
|
+
const rest = libPath.slice(row.libPath.length);
|
|
178
|
+
if (rest && rest.indexOf('/') === -1) { return row.specifier + rest; }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
95
184
|
/** An import map resolves an address only if it is an absolute URL or starts with '/', './' or '../'. */
|
|
96
185
|
function _isMapAddress(url) {
|
|
97
186
|
if (/^(\/|\.\/|\.\.\/)/.test(url)) { return true; }
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* EXHIBITS is the ONE source for the front-page tiles, their GYM deep links and the scene card:
|
|
9
9
|
* nine rows in front-page order, each
|
|
10
10
|
* `{ kind, id, mode, title, tile, what, why, how, controls, still }`.
|
|
11
|
-
* - `kind` is 'rung' (a walk-through rung, lib/shim/registry/walkthrough.js)
|
|
12
|
-
*
|
|
13
|
-
* - `mode` is a rung's declared mode (null for a single-mode rung
|
|
11
|
+
* - `kind` is 'rung' (a walk-through rung, lib/shim/registry/walkthrough.js); `id` resolves there. A row of
|
|
12
|
+
* any other kind THROWS naming the rung form.
|
|
13
|
+
* - `mode` is a rung's declared mode (null for a single-mode rung).
|
|
14
14
|
* - `title` is the tile's and the card's title; `tile` is the published one-sentence tile line.
|
|
15
15
|
* - `what` (What you are seeing), `why` (Why it is impossible the usual way) and `how` (How FRQTL
|
|
16
16
|
* gets it) are the card's copy, CARD_PARTS, every one REQUIRED on every row.
|
|
@@ -47,14 +47,13 @@
|
|
|
47
47
|
* @module exhibits
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
-
import { EXPERIMENTS, experimentById, experimentIs3D } from './experiments.js';
|
|
51
50
|
import {
|
|
52
51
|
WALKTHROUGH, walkthroughById, walkthroughModes, defaultWalkthroughModeId, walkthroughIs3D, effectiveRenderState
|
|
53
52
|
} from './walkthrough.js';
|
|
54
53
|
import { assertCopyText, assertCopyParagraphs } from './copy-gate.js';
|
|
55
54
|
|
|
56
55
|
/** The scene kinds a roster row (and a player deep link) can name. */
|
|
57
|
-
export const EXHIBIT_KINDS = Object.freeze(['rung'
|
|
56
|
+
export const EXHIBIT_KINDS = Object.freeze(['rung']);
|
|
58
57
|
|
|
59
58
|
/** The copy parts a run-once CARD shows, in order; every roster row carries all of them. */
|
|
60
59
|
export const CARD_PARTS = Object.freeze(['what', 'why', 'how']);
|
|
@@ -287,15 +286,6 @@ function rungOrThrow(id) {
|
|
|
287
286
|
return rung;
|
|
288
287
|
}
|
|
289
288
|
|
|
290
|
-
function experimentOrThrow(id) {
|
|
291
|
-
const entry = experimentById(id);
|
|
292
|
-
if (!entry) {
|
|
293
|
-
throw new Error('[exhibits] unknown experiment "' + id + '" — declared experiments: [' +
|
|
294
|
-
EXPERIMENTS.map((e) => e.id).join(', ') + ']. Add it to lib/shim/registry/experiments.js first.');
|
|
295
|
-
}
|
|
296
|
-
return entry;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
289
|
/** A rung's effective mode: the declared one (validated), or null for a single-mode rung. */
|
|
300
290
|
function rungMode(rung, modeId) {
|
|
301
291
|
const eff = (modeId !== undefined && modeId !== null) ? modeId : defaultWalkthroughModeId(rung);
|
|
@@ -322,8 +312,8 @@ function assertNotPasted(part, where) {
|
|
|
322
312
|
}
|
|
323
313
|
|
|
324
314
|
/**
|
|
325
|
-
* assertRoster — the load-time validator: every row
|
|
326
|
-
*
|
|
315
|
+
* assertRoster — the load-time validator: every row is a rung whose id resolves in the walk-through
|
|
316
|
+
* registry, its mode is declared, no scene repeats, no unknown field, every
|
|
327
317
|
* row has a title, a tile line and every CARD_PARTS part, `controls` is non-empty when present, all
|
|
328
318
|
* copy passes the copy gate, and no part pastes the invitation or an intro paragraph. Exported so a
|
|
329
319
|
* test can show each refusal on a planted roster. @returns the normalized, frozen rows (with `order`).
|
|
@@ -337,17 +327,10 @@ export function assertRoster(rows) {
|
|
|
337
327
|
}
|
|
338
328
|
});
|
|
339
329
|
if (EXHIBIT_KINDS.indexOf(row.kind) === -1) {
|
|
340
|
-
throw new Error('[exhibits] row ' + order + ' kind ' + JSON.stringify(row.kind) + ' —
|
|
341
|
-
|
|
342
|
-
let mode = null;
|
|
343
|
-
if (row.kind === 'rung') {
|
|
344
|
-
mode = rungMode(rungOrThrow(row.id), row.mode);
|
|
345
|
-
} else {
|
|
346
|
-
experimentOrThrow(row.id);
|
|
347
|
-
if (row.mode !== undefined) {
|
|
348
|
-
throw new Error('[exhibits] experiment row "' + row.id + '" names a mode — only a rung has modes.');
|
|
349
|
-
}
|
|
330
|
+
throw new Error('[exhibits] row ' + order + ' kind ' + JSON.stringify(row.kind) + ' — an exhibit is a walk-through rung: ' +
|
|
331
|
+
'{ kind: \'rung\', id: <a lib/shim/registry/walkthrough.js id>, mode? } (EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + ').');
|
|
350
332
|
}
|
|
333
|
+
const mode = rungMode(rungOrThrow(row.id), row.mode);
|
|
351
334
|
if (row.title === undefined || row.title === null) {
|
|
352
335
|
throw new Error('[exhibits] row "' + row.id + '" has no title — every card is titled.');
|
|
353
336
|
}
|
|
@@ -380,15 +363,14 @@ export function exhibitsOfKind(kind) {
|
|
|
380
363
|
* The roster row for a scene, or null when the scene is not on the roster. A null is a DECLARED
|
|
381
364
|
* outcome the caller records (the player plays such a scene with no card: 'not-offered-no-roster-row').
|
|
382
365
|
* The scene itself must exist: an unknown kind, id or mode throws.
|
|
383
|
-
* @param {{kind: 'rung'
|
|
366
|
+
* @param {{kind: 'rung', id: string, modeId?: string|null}} scene
|
|
384
367
|
*/
|
|
385
368
|
export function exhibitFor(scene) {
|
|
386
369
|
const kind = scene && scene.kind;
|
|
387
370
|
if (EXHIBIT_KINDS.indexOf(kind) === -1) {
|
|
388
371
|
throw new Error('[exhibits] unknown scene kind ' + JSON.stringify(kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
|
|
389
372
|
}
|
|
390
|
-
|
|
391
|
-
if (kind === 'rung') { mode = rungMode(rungOrThrow(scene.id), scene.modeId); } else { experimentOrThrow(scene.id); }
|
|
373
|
+
const mode = rungMode(rungOrThrow(scene.id), scene.modeId);
|
|
392
374
|
for (let i = 0; i < EXHIBITS.length; i++) {
|
|
393
375
|
const e = EXHIBITS[i];
|
|
394
376
|
if (e.kind === kind && e.id === scene.id && e.mode === mode) { return e; }
|
|
@@ -414,21 +396,20 @@ export const JOYSTICK_EXPLAINER = Object.freeze([
|
|
|
414
396
|
* sceneKey — the run-once seen-key of a scene: kind, id and, for a multi-mode rung, its EFFECTIVE
|
|
415
397
|
* mode (the one normalizer, rungMode); never a bare id, so it cannot collide with a tab key.
|
|
416
398
|
* Throws for an unknown kind, id or mode.
|
|
417
|
-
* @param {{kind: 'rung'
|
|
399
|
+
* @param {{kind: 'rung', id: string, modeId?: string|null}} scene
|
|
418
400
|
*/
|
|
419
401
|
export function sceneKey(scene) {
|
|
420
402
|
const kind = scene && scene.kind;
|
|
421
403
|
if (EXHIBIT_KINDS.indexOf(kind) === -1) {
|
|
422
404
|
throw new Error('[exhibits] unknown scene kind ' + JSON.stringify(kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
|
|
423
405
|
}
|
|
424
|
-
|
|
425
|
-
if (kind === 'rung') { mode = rungMode(rungOrThrow(scene.id), scene.modeId); } else { experimentOrThrow(scene.id); }
|
|
406
|
+
const mode = rungMode(rungOrThrow(scene.id), scene.modeId);
|
|
426
407
|
return 'scene-' + kind + '-' + scene.id + (mode ? '-' + mode : '');
|
|
427
408
|
}
|
|
428
409
|
|
|
429
410
|
/**
|
|
430
411
|
* The run-once card of a ROSTER row: title, then `what`, `why`, `how`, `controls` when present, the
|
|
431
|
-
* JOYSTICK_EXPLAINER on a 3D scene (3D read from walkthroughIs3D
|
|
412
|
+
* JOYSTICK_EXPLAINER on a 3D scene (3D read from walkthroughIs3D), and
|
|
432
413
|
* SHOWCASE_INVITE last, by reference. Composed from the row and the showcase constants only: no
|
|
433
414
|
* registry prose ever. The bag's { seenKey, title, paragraphs } is what lib/components/run-once-card
|
|
434
415
|
* renders.
|
|
@@ -440,7 +421,7 @@ export function showcaseCard(row) {
|
|
|
440
421
|
throw new Error('[exhibits] showcaseCard takes a roster row (an EXHIBITS element, e.g. exhibitFor(scene)); got ' +
|
|
441
422
|
JSON.stringify(row && { kind: row.kind, id: row.id }) + '.');
|
|
442
423
|
}
|
|
443
|
-
const is3D =
|
|
424
|
+
const is3D = walkthroughIs3D(row.id, row.mode);
|
|
444
425
|
let paragraphs = CARD_PARTS.map((p) => Object.freeze([row[p]]));
|
|
445
426
|
if (row.controls !== null) { paragraphs = paragraphs.concat([Object.freeze([row.controls])]); }
|
|
446
427
|
if (is3D) { paragraphs = paragraphs.concat(JOYSTICK_EXPLAINER); }
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* walkthrough.js the walk-through rungs: walkthroughById, effectiveRenderState, modes
|
|
11
11
|
* exhibits.js the showcase roster
|
|
12
|
-
*
|
|
12
|
+
* scene-roster.js sceneGroups() — THE scene list both views show (the showcase, then the walk-through groups)
|
|
13
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';
|
|
17
17
|
export * from './exhibits.js';
|
|
18
|
-
export * from './
|
|
18
|
+
export * from './scene-roster.js';
|
|
19
19
|
export * from './scene-source.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scene-roster.js — THE scene list both views show (the GYM's select and STUDIO's dropdown): one owner, read by
|
|
5
|
+
* both, so the two lists are the same rows in the same order.
|
|
6
|
+
*
|
|
7
|
+
* SHOWCASE_GROUP the label of the showcase group ('Impossible things')
|
|
8
|
+
* sceneGroups() → [{ label, rows: [{ id, title }] }] — the showcase roster first (EXHIBITS, in tile order,
|
|
9
|
+
* each row its exhibit title), then every walk-through rung in WALKTHROUGH order under its
|
|
10
|
+
* lab group (walkthroughGroup), with NO rung removed from its own group: a showcase rung
|
|
11
|
+
* appears twice, and both rows name the same rung (a deep link selects it either way).
|
|
12
|
+
*
|
|
13
|
+
* A row is a scene id a host opens by ?walkthrough=<id>. A host keeps its own non-scene rows (STUDIO's blank world
|
|
14
|
+
* and saved projects) around these groups; the GYM's default scene is the first row.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { EXHIBITS } from './exhibits.js';
|
|
18
|
+
import { WALKTHROUGH, walkthroughGroup } from './walkthrough.js';
|
|
19
|
+
|
|
20
|
+
/** The showcase group's label. */
|
|
21
|
+
export const SHOWCASE_GROUP = 'Impossible things';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @returns {Array<{label: string, rows: Array<{id: string, title: string}>}>} a new array on every call
|
|
25
|
+
*/
|
|
26
|
+
export function sceneGroups() {
|
|
27
|
+
var groups = [{ label: SHOWCASE_GROUP, rows: EXHIBITS.map(function (e) { return { id: e.id, title: e.title }; }) }];
|
|
28
|
+
WALKTHROUGH.forEach(function (r) {
|
|
29
|
+
var g = walkthroughGroup(r);
|
|
30
|
+
var last = groups[groups.length - 1];
|
|
31
|
+
if (groups.length === 1 || last.label !== g) { groups.push({ label: g, rows: [] }); last = groups[groups.length - 1]; }
|
|
32
|
+
last.rows.push({ id: r.id, title: r.title });
|
|
33
|
+
});
|
|
34
|
+
return groups;
|
|
35
|
+
}
|