@descix/frqtl-node 0.6.1-p3.3 → 0.7.0-p0
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 +3 -3
- package/README.md +4 -3
- package/dist/dist-manifest.json +26 -56
- package/dist/frqtl/experiments/_shared/empty-sketch.js +17 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +6 -6
- package/dist/frqtl/objects/setup/setupFxSmoke.js +2 -2
- package/dist/frqtl/objects/setup/setupG3Collide.js +1 -1
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +2 -2
- package/dist/frqtl/objects/v1-replacements.js +1 -1
- package/dist/frqtl/parts/OvenBox.js +2 -2
- package/dist/frqtl/parts/SphereEmitter.js +1 -1
- package/dist/shim/engine/router.js +18 -6
- package/dist/shim/engine/site-assemble.mjs +268 -0
- package/dist/shim/engine/site-graph.js +42 -14
- package/dist/shim/host/device.js +1 -1
- package/dist/shim/index.js +3 -7
- package/dist/shim/notebook/catalog.js +2 -2
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/knob.js +1 -1
- package/dist/shim/notebook/notebook-knobs.js +1 -1
- package/dist/shim/project/specifiers.js +8 -8
- package/dist/shim/registry/copy-gate.js +3 -3
- package/dist/shim/registry/index.js +9 -12
- package/dist/shim/registry/roster-link.js +142 -0
- package/dist/shim/registry/roster.js +595 -0
- package/dist/shim/registry/scene-source.js +80 -105
- package/dist/shim/registry/studio-landing.js +15 -7
- package/dist/shim/render/sun-look.js +1 -2
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/dist/frqtl/scenes/absorbing-wall-3d.js +0 -37
- package/dist/frqtl/scenes/absorbing-wall.js +0 -36
- package/dist/frqtl/scenes/atomic-model.js +0 -91
- package/dist/frqtl/scenes/big-bang.js +0 -49
- package/dist/frqtl/scenes/blackbody.js +0 -84
- package/dist/frqtl/scenes/circuit-sat.js +0 -131
- package/dist/frqtl/scenes/collision-matrix-3d.js +0 -110
- package/dist/frqtl/scenes/counting-wall-3d.js +0 -40
- package/dist/frqtl/scenes/counting-wall.js +0 -42
- package/dist/frqtl/scenes/double-slit.js +0 -34
- package/dist/frqtl/scenes/dse-one-emitter.js +0 -200
- package/dist/frqtl/scenes/dse-one-slit.js +0 -118
- package/dist/frqtl/scenes/dse-photoelectric.js +0 -74
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +0 -279
- package/dist/frqtl/scenes/dse-two-emitters.js +0 -201
- package/dist/frqtl/scenes/dse-two-slits.js +0 -136
- package/dist/frqtl/scenes/empty.js +0 -17
- package/dist/frqtl/scenes/laser-buildup.js +0 -56
- package/dist/frqtl/scenes/mirror-3d.js +0 -60
- package/dist/frqtl/scenes/mirror-route.js +0 -164
- package/dist/frqtl/scenes/mirror.js +0 -42
- package/dist/frqtl/scenes/omni-3d.js +0 -30
- package/dist/frqtl/scenes/omni-source.js +0 -25
- package/dist/frqtl/scenes/self-interference.js +0 -42
- package/dist/frqtl/scenes/smoking-gun.js +0 -126
- package/dist/frqtl/scenes/straight-walk.js +0 -25
- package/dist/frqtl/scenes/sun.js +0 -42
- package/dist/frqtl/scenes/two-source.js +0 -42
- package/dist/frqtl/scenes/wave.js +0 -29
- package/dist/shim/engine/artifact-shell.js +0 -211
- package/dist/shim/registry/exhibits.js +0 -436
- package/dist/shim/registry/scene-roster.js +0 -35
- package/dist/shim/registry/showcase-link.js +0 -61
- package/dist/shim/registry/walkthrough.js +0 -752
|
@@ -1,141 +1,116 @@
|
|
|
1
1
|
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* scene-source.js — the ONE reader of a
|
|
4
|
+
* scene-source.js — the ONE reader of a scene as a PROJECT, in a page AND in node.
|
|
5
5
|
*
|
|
6
|
-
* A scene is ONE sketch file
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* here, so the id → path → URL → project chain exists once.
|
|
6
|
+
* A scene is ONE sketch file: a module whose default export is `buildExperiment(exp)`, holding the whole scene
|
|
7
|
+
* (world, parts, render intent). A host's roster names each scene's file relative to the roster
|
|
8
|
+
* (roster.js::sceneUrlOf); the Empty Universe is the package's own sketch
|
|
9
|
+
* (lib/frqtl/experiments/_shared/empty-sketch.js), read through the router like any package file.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* → Promise<{ manifest, files, renderState }>
|
|
15
|
-
* files { 'frqtl.json': serializeManifest(manifestFor({ name: row.id, entry: 'sketch.js' })),
|
|
11
|
+
* projectOf(roster, id, { fetch?, mode? }) → Promise<{ manifest, files, renderState }>
|
|
12
|
+
* files { 'frqtl.json': serializeManifest(manifestFor({ name: id, entry: 'sketch.js' })),
|
|
16
13
|
* 'sketch.js': <the scene file's text> } — the folder a host boots
|
|
17
|
-
* (bootProject(mount, { files, mint }, { renderState }), no entry needed)
|
|
18
14
|
* manifest that frqtl.json's normalized bag
|
|
19
|
-
* renderState effectiveRenderState(row, mode) —
|
|
20
|
-
*
|
|
15
|
+
* renderState effectiveRenderState(row, mode) — boot facts ride BESIDE the folder, never in frqtl.json
|
|
16
|
+
* emptyProjectOf({ router, fetch? }) → the same shape for the Empty Universe (name EMPTY_PROJECT_NAME,
|
|
17
|
+
* renderState normalizeSavedRenderState({}))
|
|
18
|
+
* EMPTY_PROJECT_NAME 'empty'
|
|
19
|
+
* EMPTY_SKETCH_PATH the Empty Universe's router path (a package builder places that file
|
|
20
|
+
* in every dist this module ships in: sdk/_build/hosts.mjs)
|
|
21
21
|
*
|
|
22
|
-
*
|
|
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):
|
|
25
|
-
* - `opts.router` — the host's own (the IDE's engineRouter); a host always passes it.
|
|
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 lib/shim/project/read-text.js readText (a capability split on the resolved URL: a `file:` URL
|
|
30
|
-
* through node's fs, anything else fetched).
|
|
22
|
+
* The read is lib/shim/project/read-text.js readText (a `file:` URL through node's fs, anything else fetched).
|
|
31
23
|
*
|
|
32
|
-
* FAIL-LOUD:
|
|
33
|
-
*
|
|
24
|
+
* FAIL-LOUD: a non-bag roster, an undeclared id or mode, an unknown option, a non-200, an unreadable file or an
|
|
25
|
+
* empty body REJECTS naming the scene and the URL. A blank scene is never a result.
|
|
34
26
|
*
|
|
35
27
|
* Zero physics: this moves text. The scene's physics is whatever its file composes.
|
|
36
28
|
*/
|
|
37
29
|
|
|
38
30
|
'use strict';
|
|
39
31
|
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
32
|
+
import { assertRosterBag, walkthroughById, effectiveRenderState, sceneUrlOf } from './roster.js';
|
|
33
|
+
import { normalizeSavedRenderState } from '../render-state/index.js';
|
|
42
34
|
import { MANIFEST_FILE, manifestFor, serializeManifest } from '../project/manifest.js';
|
|
43
35
|
import { SKETCH_ENTRY } from '../project/link.js';
|
|
44
36
|
import { readText } from '../project/read-text.js';
|
|
45
37
|
|
|
46
|
-
/** The
|
|
47
|
-
export
|
|
38
|
+
/** The project name of the Empty Universe (its frqtl.json `name`). */
|
|
39
|
+
export const EMPTY_PROJECT_NAME = 'empty';
|
|
48
40
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
* lib/shim/ at dist/shim/ (sdk/_build/lib-dist-placement.cjs), so this file is
|
|
52
|
-
* dist/shim/registry/scene-source.js. verify-publish-surface reads every rung through it on the
|
|
53
|
-
* built dist, so a moved placement fails there.
|
|
54
|
-
*/
|
|
55
|
-
var PACKAGE_ROOT_FROM_HERE = '../../';
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The blank world — STUDIO's "Empty Universe" — is itself a scene file (the empty page states no
|
|
59
|
-
* world of its own). A row-shaped handle so it reads through the same `projectOf`.
|
|
60
|
-
*/
|
|
61
|
-
export var EMPTY_SCENE = Object.freeze({ id: 'empty', source: 'scenes/empty.js' });
|
|
41
|
+
/** The Empty Universe's sketch, a lib-identity path every router profile resolves. */
|
|
42
|
+
export const EMPTY_SKETCH_PATH = 'experiments/_shared/empty-sketch.js';
|
|
62
43
|
|
|
63
|
-
|
|
44
|
+
const PROJECT_OF = 'projectOf(roster, id, { fetch, mode })';
|
|
45
|
+
const EMPTY_PROJECT_OF = 'emptyProjectOf({ router, fetch })';
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
export function scenePath(rung) {
|
|
70
|
-
var src = rung && rung.source;
|
|
71
|
-
if (typeof src !== 'string' || src.indexOf(SCENES_PREFIX) !== 0 || !/\.js$/.test(src)) {
|
|
72
|
-
throw new Error('[scene-source] rung "' + (rung && rung.id) + '" declares source ' +
|
|
73
|
-
JSON.stringify(src) + ' — a scene row carries the path of its sketch file, ' +
|
|
74
|
-
"'scenes/<file>.js' (lib/frqtl/scenes/). (FAIL-LOUD.)");
|
|
47
|
+
function assertOpts(opts, allowed, signature) {
|
|
48
|
+
const o = opts === undefined ? {} : opts;
|
|
49
|
+
if (o === null || typeof o !== 'object' || Array.isArray(o)) {
|
|
50
|
+
throw new Error('[scene-source] ' + signature + ': options are an object (got ' + JSON.stringify(opts) + '). (FAIL-LOUD.)');
|
|
75
51
|
}
|
|
76
|
-
|
|
52
|
+
Object.keys(o).forEach((k) => {
|
|
53
|
+
if (allowed.indexOf(k) === -1) {
|
|
54
|
+
throw new Error('[scene-source] ' + signature + ' has no option "' + k + '" — options: ' + allowed.join(', ') +
|
|
55
|
+
(k === 'router' ? '; a roster scene\'s URL is sceneUrlOf(roster, row), not a router path' : '') + '. (FAIL-LOUD.)');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return o;
|
|
77
59
|
}
|
|
78
60
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
function folderOf(name, url, label, opts, renderState) {
|
|
62
|
+
return readText(url, { fetch: opts.fetch }).catch((e) => {
|
|
63
|
+
throw new Error('[scene-source] ' + label + ' — ' + ((e && e.message) || e) + '. (FAIL-LOUD.)');
|
|
64
|
+
}).then((t) => {
|
|
65
|
+
if (typeof t !== 'string' || !t.trim()) {
|
|
66
|
+
throw new Error('[scene-source] ' + label + ' — ' + url + ' has an empty body. (FAIL-LOUD.)');
|
|
85
67
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
return scene;
|
|
68
|
+
const manifest = manifestFor({ name, entry: SKETCH_ENTRY });
|
|
69
|
+
const files = {};
|
|
70
|
+
files[MANIFEST_FILE] = serializeManifest(manifest);
|
|
71
|
+
files[SKETCH_ENTRY] = t;
|
|
72
|
+
return { manifest, files, renderState };
|
|
73
|
+
});
|
|
93
74
|
}
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
76
|
+
/**
|
|
77
|
+
* A roster scene as the project folder a host boots, with its renderState beside it.
|
|
78
|
+
* @param {object} roster a normalized roster bag (roster.js normalizeRoster / roster-link.js loadRoster)
|
|
79
|
+
* @param {string} id a scene id the roster declares
|
|
80
|
+
* @param {{fetch?: function, mode?: string|null}} [opts] fetch: injectable for a non-file URL; mode: a declared
|
|
81
|
+
* mode of the scene (absent: its default) for the renderState
|
|
82
|
+
* @returns {Promise<{manifest: object, files: Object<string,string>, renderState: object}>}
|
|
83
|
+
*/
|
|
84
|
+
export function projectOf(roster, id, opts) {
|
|
85
|
+
return Promise.resolve().then(() => {
|
|
86
|
+
assertRosterBag(roster, PROJECT_OF);
|
|
87
|
+
const o = assertOpts(opts, ['fetch', 'mode'], PROJECT_OF);
|
|
88
|
+
if (typeof id !== 'string' || id === '') {
|
|
89
|
+
throw new Error('[scene-source] ' + PROJECT_OF + ' takes a scene id string (got ' + JSON.stringify(id) + '). (FAIL-LOUD.)');
|
|
90
|
+
}
|
|
91
|
+
const row = walkthroughById(roster, id);
|
|
92
|
+
if (!row) {
|
|
93
|
+
throw new Error('[scene-source] ' + PROJECT_OF + ': "' + id + '" is not a declared scene — declared: [' +
|
|
94
|
+
roster.rows.map((r) => r.id).join(', ') + ']. (FAIL-LOUD.)');
|
|
95
|
+
}
|
|
96
|
+
const renderState = effectiveRenderState(row, o.mode); // an undeclared mode rejects before the read
|
|
97
|
+
return folderOf(row.id, sceneUrlOf(roster, row), 'scene "' + row.id + '"', o, renderState);
|
|
98
|
+
});
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
|
-
*
|
|
102
|
-
* @param {
|
|
103
|
-
* @param {{router?:{resolve:function(string):string}, fetch?:function, mode?:string|null}} [opts]
|
|
104
|
-
* router — the host's engineRouter (absent: the package's own dist root);
|
|
105
|
-
* fetch — the fetch used for a non-file URL (defaults to the global; injectable for tests);
|
|
106
|
-
* mode — a declared walk-through mode (absent: the rung's default) for the renderState.
|
|
102
|
+
* The Empty Universe as a project folder: the package sketch read through the host's router.
|
|
103
|
+
* @param {{router: {resolve: function(string): string}, fetch?: function}} opts
|
|
107
104
|
* @returns {Promise<{manifest: object, files: Object<string,string>, renderState: object}>}
|
|
108
105
|
*/
|
|
109
|
-
export function
|
|
110
|
-
return Promise.resolve().then(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
OPT_KEYS.join(', ') + '. (FAIL-LOUD.)');
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
var row = _rowOf(scene);
|
|
119
|
-
var rel = scenePath(row);
|
|
120
|
-
var renderState = effectiveRenderState(row, opts.mode); // an undeclared mode rejects before the read
|
|
121
|
-
var router = opts.router || _packageRouter();
|
|
122
|
-
if (typeof router.resolve !== 'function') {
|
|
123
|
-
throw new Error('[scene-source] opts.router has no resolve() — pass an engineRouter ' +
|
|
124
|
-
'(lib/shim/engine/router.js) to resolve ' + rel + '. (FAIL-LOUD.)');
|
|
106
|
+
export function emptyProjectOf(opts) {
|
|
107
|
+
return Promise.resolve().then(() => {
|
|
108
|
+
const o = assertOpts(opts, ['router', 'fetch'], EMPTY_PROJECT_OF);
|
|
109
|
+
if (!o.router || typeof o.router.resolve !== 'function') {
|
|
110
|
+
throw new Error('[scene-source] ' + EMPTY_PROJECT_OF + ' needs the host\'s engineRouter ' +
|
|
111
|
+
'(lib/shim/engine/router.js) to resolve ' + EMPTY_SKETCH_PATH + '. (FAIL-LOUD.)');
|
|
125
112
|
}
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
throw new Error('[scene-source] scene "' + row.id + '" — ' + ((e && e.message) || e) + '. (FAIL-LOUD.)');
|
|
129
|
-
}).then(function (t) {
|
|
130
|
-
if (typeof t !== 'string' || !t.trim()) {
|
|
131
|
-
throw new Error('[scene-source] scene "' + row.id + '" — ' + url +
|
|
132
|
-
' has an empty body. (FAIL-LOUD.)');
|
|
133
|
-
}
|
|
134
|
-
var manifest = manifestFor({ name: row.id, entry: SKETCH_ENTRY });
|
|
135
|
-
var files = {};
|
|
136
|
-
files[MANIFEST_FILE] = serializeManifest(manifest);
|
|
137
|
-
files[SKETCH_ENTRY] = t;
|
|
138
|
-
return { manifest: manifest, files: files, renderState: renderState };
|
|
139
|
-
});
|
|
113
|
+
const url = o.router.resolve(EMPTY_SKETCH_PATH);
|
|
114
|
+
return folderOf(EMPTY_PROJECT_NAME, url, 'the Empty Universe', o, normalizeSavedRenderState({}));
|
|
140
115
|
});
|
|
141
116
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* studio-landing.js — the ONE record of how STUDIO resolved the deep link it opened with, as host-view.js records
|
|
5
5
|
* its view outcome: STUDIO's resolver (lib/features/ide/studio-landing-of.js::studioLandingOf) decides the row and
|
|
6
6
|
* AuthorView.jsx writes it on `document.documentElement` under STUDIO_LANDING_ATTR, through recordStudioLanding,
|
|
7
|
-
* and a gate that opens a link (a showcase tile,
|
|
7
|
+
* and a gate that opens a link (a showcase tile, roster.js showcaseHref) reads that attribute instead of STUDIO's DOM or
|
|
8
8
|
* its error copy.
|
|
9
9
|
*
|
|
10
10
|
* STUDIO_LANDING_ATTR the <html> attribute carrying the outcome id
|
|
@@ -33,23 +33,31 @@ export const STUDIO_LANDING_ATTR = 'data-frqtl-studio-landing';
|
|
|
33
33
|
/** The closed table of STUDIO deep-link landings, in the order the resolver tests them (lib/features/ide/test/studio-landing-of.test.mjs holds it). */
|
|
34
34
|
export const STUDIO_LANDING_OUTCOMES = Object.freeze([
|
|
35
35
|
{ id: 'refused-key', runs: false,
|
|
36
|
-
means: 'the link carries a key STUDIO does not own, or a deleted key (deep-link.js deepLinkRefusalOf): the refusal is on the deep-link strip and the opening
|
|
36
|
+
means: 'the link carries a key STUDIO does not own, or a deleted key (deep-link.js deepLinkRefusalOf): the refusal is on the deep-link strip and the opening scene opens' },
|
|
37
37
|
{ id: 'conflict', runs: false,
|
|
38
|
-
means: 'the link names
|
|
38
|
+
means: 'the link names more than one scene source (two of walkthrough, project and scene), or a renderer beside a walkthrough or a project, which carry their own: nothing opens' },
|
|
39
39
|
{ id: 'mode-without-walkthrough', runs: false,
|
|
40
40
|
means: 'the link names a mode and no walkthrough: nothing opens' },
|
|
41
|
+
{ id: 'renderer-without-scene', runs: false,
|
|
42
|
+
means: 'the link names a renderer and no scene; a renderer is chosen only for a scene link: nothing opens' },
|
|
41
43
|
{ id: 'unknown-walkthrough', runs: false,
|
|
42
|
-
means: 'the linked walkthrough is not
|
|
44
|
+
means: 'the linked walkthrough is not a scene of the loaded roster (walkthroughById(roster, id)): nothing opens' },
|
|
43
45
|
{ id: 'unknown-mode', runs: false,
|
|
44
|
-
means: 'the linked mode is not one the linked
|
|
46
|
+
means: 'the linked mode is not one the linked scene declares (walkthroughModes): nothing opens' },
|
|
47
|
+
{ id: 'scene-refused-origin', runs: false,
|
|
48
|
+
means: 'the linked scene URL is on neither the page\'s origin nor the roster\'s (roster-link.js sceneOriginRefusalOf): the strip names the rule and both origins; nothing opens' },
|
|
49
|
+
{ id: 'scene-unreadable', runs: false,
|
|
50
|
+
means: 'the linked scene URL is allowed but its read failed (a non-200, an empty body, or not a .js sketch): nothing opens' },
|
|
45
51
|
{ id: 'project-unreadable', runs: false,
|
|
46
52
|
means: 'the linked saved project is present but its folder is refused by the load: nothing opens' },
|
|
47
53
|
{ id: 'project-not-found', runs: false,
|
|
48
54
|
means: 'no saved project of the linked name in this browser: nothing opens' },
|
|
49
55
|
{ id: 'ok', runs: true,
|
|
50
|
-
means: 'the linked walkthrough (in the linked mode, else its default)
|
|
56
|
+
means: 'the linked walkthrough (in the linked mode, else its default), saved project or scene resolved and was handed to STUDIO\'s selection path; the scene starts on load' },
|
|
51
57
|
{ id: 'first-contact', runs: false,
|
|
52
|
-
means: 'no STUDIO deep link: the
|
|
58
|
+
means: 'no STUDIO deep link: the roster\'s first-contact scene (firstContactOf(roster)) opens' },
|
|
59
|
+
{ id: 'first-contact-empty', runs: false,
|
|
60
|
+
means: 'no STUDIO deep link and the roster declares no first contact: the Empty Universe opens' },
|
|
53
61
|
].map(Object.freeze));
|
|
54
62
|
|
|
55
63
|
const IDS = new Set(STUDIO_LANDING_OUTCOMES.map((r) => r.id));
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
* `godrayMode` 'radial' is the sun's own (its rays fan from one light point, the source frames' centroid).
|
|
14
14
|
* The camera is NOT here: a pose is a rung row's view, not a look.
|
|
15
15
|
*
|
|
16
|
-
* Readers:
|
|
17
|
-
* (`@descix/frqtl-web/shim/render/sun-look.js`). Zero physics: render
|
|
16
|
+
* Readers: a scene sketch, by its package specifier (`@descix/frqtl-web/shim/render/sun-look.js`). Zero physics: render
|
|
18
17
|
* options are read only by the renderer.
|
|
19
18
|
*/
|
|
20
19
|
|
package/llms.txt
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# @descix/frqtl-node
|
|
2
2
|
|
|
3
3
|
> The FRQTL physics engine (compiled Rust → WASM) for node: the same modules as @descix/frqtl-web, plus
|
|
4
|
-
> `bootNode` and the hosted eyes. 0.
|
|
4
|
+
> `bootNode` and the hosted eyes. 0.7.0-p0 on npm dist-tag `next`. Node ≥ 22.12.
|
|
5
5
|
|
|
6
6
|
- Install: `npm install @descix/frqtl-node`
|
|
7
7
|
- `import { bootNode, Wall } from '@descix/frqtl-node'` (require() works too); `await bootNode()` → the engine handle.
|
|
8
8
|
- `./eyes`: `openHostedEyes({ sceneRecord, sceneSource: { kind: 'project', files, entry }, pageUrl })`.
|
|
9
9
|
- `./project`: `linkProject` links a scene file against this package's own copy.
|
|
10
|
+
- `./registry`: the roster contract: `loadRoster(<file: URL of a host's roster>)` → the bag every reader takes first (`projectOf(roster, id, { mode })`, `effectiveRenderState(row, mode)`, `showcaseHref(roster, card)`, …); the package ships no scene rows.
|
|
11
|
+
- See it run: https://egpt-frqtl.descix.net
|
|
10
12
|
- `./host-view`: `declareHostView`, `HOST_VIEW_OUTCOMES`, `CONTRACT_VIOLATIONS`, `HOST_SDK_SCRIPT` — the same declarer as @descix/frqtl-web/host-view, pure, for a node test of a call site.
|
|
11
13
|
- `./register`: `node --import @descix/frqtl-node/register scene.mjs` resolves a scene's `@descix/frqtl-web/...` imports to this package.
|
|
12
|
-
- Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.
|
|
14
|
+
- Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.7.0-p0/CLAUDE.md
|
|
13
15
|
- Acceptance is visual; no CLI ships. No account, credits or tokens are needed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/frqtl-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-p0",
|
|
4
4
|
"description": "FRQTL for node \u2014 the discrete fractal physics engine (compiled Rust \u2192 WASM), its parts as ES modules, the node engine door (bootNode) and the hosted eyes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SCENE — 1B · Absorbing Wall (3D)
|
|
5
|
-
*
|
|
6
|
-
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
|
|
7
|
-
* (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
|
|
8
|
-
* the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
|
|
9
|
-
* objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
|
|
10
|
-
* (rung cl1b-absorbing-wall-3d).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
|
|
14
|
-
|
|
15
|
-
// Collision Lab 1B · 3D — a volumetric (spherical) source into a 3D ABSORB wall
|
|
16
|
-
// slab (z-slab centered on 0 so the 3D spray collides); orbit the camera to view.
|
|
17
|
-
const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
|
|
18
|
-
const cy = Math.round(H / 2);
|
|
19
|
-
|
|
20
|
-
export const parts = {
|
|
21
|
-
source: [SphereEmitter, {
|
|
22
|
-
preset: 'spherical', is2D: false, center: [Math.round(W * 0.15), cy],
|
|
23
|
-
chargeMode: 'random', fundamentalDimension: 2, wavelength: 32,
|
|
24
|
-
burstSize: 24, emitEveryTicks: 1
|
|
25
|
-
}],
|
|
26
|
-
absorber: [Wall, {
|
|
27
|
-
rect: [Math.round(W * 0.6), Math.round(H * 0.2), 20, Math.round(H * 0.6)],
|
|
28
|
-
z: -80, depth: 160,
|
|
29
|
-
collisionBehavior: 'absorb', capacity: 64,
|
|
30
|
-
color: [150, 90, 210]
|
|
31
|
-
}],
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export default function buildExperiment(exp) {
|
|
35
|
-
exp.createScene(W, H, 600, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
|
|
36
|
-
exp.declare(parts);
|
|
37
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SCENE — 1B · Absorbing Wall
|
|
5
|
-
*
|
|
6
|
-
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
|
|
7
|
-
* (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
|
|
8
|
-
* the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
|
|
9
|
-
* objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
|
|
10
|
-
* (rung cl1b-absorbing-wall).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
|
|
14
|
-
|
|
15
|
-
// Collision Lab 1B (Absorbing Wall) — ABSORB v2 (Track A): the wall LO is backed by
|
|
16
|
-
// a REAL leaf frame; a host tick-fn tallies the killed masses into it + radiates.
|
|
17
|
-
const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
|
|
18
|
-
const cy = Math.round(H / 2);
|
|
19
|
-
|
|
20
|
-
export const parts = {
|
|
21
|
-
source: [SphereEmitter, {
|
|
22
|
-
preset: 'spherical', is2D: true, center: [Math.round(W * 0.15), cy],
|
|
23
|
-
chargeMode: 'random', fundamentalDimension: 2, wavelength: 32,
|
|
24
|
-
burstSize: 24, emitEveryTicks: 1
|
|
25
|
-
}],
|
|
26
|
-
absorber: [Wall, {
|
|
27
|
-
rect: [Math.round(W * 0.6), Math.round(H * 0.2), 20, Math.round(H * 0.6)],
|
|
28
|
-
collisionBehavior: 'absorb', capacity: 64,
|
|
29
|
-
color: [150, 90, 210]
|
|
30
|
-
}],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export default function buildExperiment(exp) {
|
|
34
|
-
exp.createCanvas(W, H, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
|
|
35
|
-
exp.declare(parts);
|
|
36
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SCENE — Atomic model
|
|
5
|
-
*
|
|
6
|
-
* A sketch file: the WHOLE scene as one `export default function buildExperiment(exp)` module. The
|
|
7
|
-
* world and the three-level frame hierarchy are built here from the engine's own primitives (leaf
|
|
8
|
-
* frames — `engine().Frame`, read when the scene builds — and parent frames from a rect). Registered by
|
|
9
|
-
* lib/shim/registry/walkthrough.js (rung atomic-model).
|
|
10
|
-
*
|
|
11
|
-
* Physics (zero new — bench H1, A1, A2, M1, M4): 48 one-pixel quarks fill an 8x8 proton frame; the
|
|
12
|
-
* proton and one electron sit in a 64x64 atom frame. There is no force equation. Each parent's
|
|
13
|
-
* compress() pulls its children one pixel toward its centre each tick (A1: a stationary parent
|
|
14
|
-
* CONFINES a child), a parent's motion is carried by its children (A2), a parent's mass is its
|
|
15
|
-
* child count (H1), and its wavelength is capacity/mass (M4) — 64/48 → the proton's oscillation.
|
|
16
|
-
* The atom is stationary: the lab frame. `bonding` (noEscape) traps each quark in its parent's
|
|
17
|
-
* live rect, so the nucleus holds together: binding emerges from compress, not from a law.
|
|
18
|
-
*
|
|
19
|
-
* Order (it seeds the run): world → the three layers → quarks (grid-filled, registered) → proton
|
|
20
|
-
* from its rect → electron (registered) → atom from its rect → electron into the atom.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { Rectangle } from '@descix/frqtl-web/parts';
|
|
24
|
-
import * as OrbitBag from '@descix/frqtl-web/compat/renderer/orbit-bag.js';
|
|
25
|
-
import { engine } from '@descix/frqtl-web/compat/engine.js';
|
|
26
|
-
|
|
27
|
-
export const parts = {};
|
|
28
|
-
|
|
29
|
-
export default function buildExperiment(exp) {
|
|
30
|
-
var W = 600, H = 600; // THE WORLD this scene states (its co-init call below)
|
|
31
|
-
|
|
32
|
-
var QUARK_LAYER = 0; // 1x1 leaves
|
|
33
|
-
var SUBATOMIC_LAYER = 3; // the proton: 8x8, capacity 64
|
|
34
|
-
var ATOM_LAYER = 6; // the atom: 64x64, capacity 4096
|
|
35
|
-
var QUARKS_PER_PROTON = 48; // the proton's mass at t=0 → wavelength 64/48
|
|
36
|
-
|
|
37
|
-
// ── WORLD — a 2D canvas + universe: quark layer fundamental, λ multiplier 1, quarks confined ──
|
|
38
|
-
exp.createCanvas(W, H, { dimension: QUARK_LAYER, wavelengthScale: 1, bonding: true });
|
|
39
|
-
exp.declare(parts);
|
|
40
|
-
var u = exp.universe;
|
|
41
|
-
|
|
42
|
-
var quarkDim = u.addDimension(QUARK_LAYER);
|
|
43
|
-
var subatomicDim = u.addDimension(SUBATOMIC_LAYER);
|
|
44
|
-
var atomDim = u.addDimension(ATOM_LAYER);
|
|
45
|
-
var subatomicW = subatomicDim.mb_w, atomW = atomDim.mb_w;
|
|
46
|
-
|
|
47
|
-
var Frame = engine().Frame, Charge = engine().Charge; // the engine's own frame primitive + charge ABI
|
|
48
|
-
var cx = Math.round(W / 2), cy = Math.round(H / 2);
|
|
49
|
-
var atomRect = new Rectangle(cx, cy, atomW, atomW, null, true);
|
|
50
|
-
var protonRect = new Rectangle(cx, cy, subatomicW, subatomicW, null, true);
|
|
51
|
-
var rect = exp.runner.canvas_rect;
|
|
52
|
-
|
|
53
|
-
// ── QUARKS — fill the proton rect column by column ─────────────────────
|
|
54
|
-
var quarkW = Math.max(1, Math.pow(2, QUARK_LAYER));
|
|
55
|
-
var quarks = [];
|
|
56
|
-
for (var x = protonRect.left; x < protonRect.left + protonRect.w && quarks.length < QUARKS_PER_PROTON; x += quarkW) {
|
|
57
|
-
for (var y = protonRect.top; y < protonRect.top + protonRect.h && quarks.length < QUARKS_PER_PROTON; y += quarkW) {
|
|
58
|
-
var q = Frame.makeLeafFrame(x, y, QUARK_LAYER, 1, 0, 0, false, Charge.POSITIVE, rect);
|
|
59
|
-
q.name = 'quark_' + quarks.length;
|
|
60
|
-
quarks.push(q);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
for (var i = 0; i < quarks.length; i++) { quarkDim.addFrame(quarks[i]); }
|
|
64
|
-
|
|
65
|
-
// ── PROTON — the parent frame of the quarks ────────────────────────────
|
|
66
|
-
var proton = subatomicDim.createFrameFromRect(protonRect, quarks, 0, 0);
|
|
67
|
-
proton.name = 'proton';
|
|
68
|
-
|
|
69
|
-
// ── ELECTRON — one negative leaf, upper-right of the atom ──────────────
|
|
70
|
-
var electron = Frame.makeLeafFrame(atomRect.left + Math.round(atomW * 0.75), atomRect.top + Math.round(atomW * 0.25),
|
|
71
|
-
QUARK_LAYER, 1, 0, 0, false, Charge.NEGATIVE, rect);
|
|
72
|
-
electron.name = 'electron';
|
|
73
|
-
quarkDim.addFrame(electron);
|
|
74
|
-
|
|
75
|
-
// ── ATOM — stationary parent of the proton; the electron joins directly ─
|
|
76
|
-
var atom = atomDim.createFrameFromRect(atomRect, [proton], 0, 0, true, true);
|
|
77
|
-
atom.name = 'atom';
|
|
78
|
-
atom.insert(electron);
|
|
79
|
-
|
|
80
|
-
u.leafQuantaWidth = quarkW;
|
|
81
|
-
u.brownianMotion = true;
|
|
82
|
-
|
|
83
|
-
// ── VIEW — frame the atom (render only: the 2D camera's `distance` is the visible physics-px
|
|
84
|
-
// height). The atom is 64 px in the world; at 1.5x its width it fills two thirds of the view,
|
|
85
|
-
// so the proton, its quarks and the electron are individually legible. ─────────────────────
|
|
86
|
-
var cam = exp.runner.cameraControl;
|
|
87
|
-
var flat = !OrbitBag.hasAngularPose(cam);
|
|
88
|
-
if (cam && typeof cam.setOrbit === 'function' && flat) {
|
|
89
|
-
cam.setOrbit({ target: { x: cx, y: cy }, distance: Math.round(atomW * 1.5) });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SCENE — Big Bang
|
|
5
|
-
*
|
|
6
|
-
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world and the
|
|
7
|
-
* point source are built here; the sugar (PointSource — deprecated V1 sugar, byte-identical), the
|
|
8
|
-
* renderer and the engine are imports. Registered by lib/shim/registry/walkthrough.js (rung big-bang).
|
|
9
|
-
*
|
|
10
|
-
* Physics (zero new — bench M1, H1, H2a-c): one radial burst at the canvas centre — 72 rays (5°) at
|
|
11
|
-
* radii 32 → 2 — of leaves with NO initial momentum (emergentPhysics). The world generates parent
|
|
12
|
-
* frames top-down (by how many frames an area can parent, brownian on), so a uniform field clumps into
|
|
13
|
-
* filaments and voids with no gravity law and no cosmological constant. The source shuts off after
|
|
14
|
-
* 20 s of ticks (1200).
|
|
15
|
-
*
|
|
16
|
-
* Order (it seeds the run): world (with its parent-generation interval) → source → the shut-off tick-fn.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { PointSource } from '@descix/frqtl-web/parts';
|
|
20
|
-
|
|
21
|
-
export const parts = {};
|
|
22
|
-
|
|
23
|
-
export default function buildExperiment(exp) {
|
|
24
|
-
var W = 600, H = 600; // THE WORLD this scene states (its co-init call below)
|
|
25
|
-
|
|
26
|
-
var SOURCE_DIAMETER = 64; // emission radii 32 → 2 (step 2)
|
|
27
|
-
var ANGLE_STEP = 5; // degrees between rays → 72 rays
|
|
28
|
-
var EMIT_TICKS = 20 * 60; // the source fires for 1200 ticks
|
|
29
|
-
|
|
30
|
-
// ── WORLD — a 2D canvas + universe: fundamental layer 0, λ multiplier 1, parents top-down ──
|
|
31
|
-
// iframeIntervalSeconds 1/60: parents are generated EVERY tick (the engine freezes the interval at
|
|
32
|
-
// init, so it rides the world cfg — at the default 1 s, Phase B ran every 60 ticks).
|
|
33
|
-
exp.createCanvas(W, H, { dimension: 0, wavelengthScale: 1, frames: true, iframeIntervalSeconds: 1 / 60 });
|
|
34
|
-
exp.declare(parts);
|
|
35
|
-
var u = exp.universe;
|
|
36
|
-
|
|
37
|
-
// ── THE SOURCE — at the centre, no initial momentum, random charge ──────
|
|
38
|
-
var source = new PointSource(u, Math.floor(W / 2), Math.floor(H / 2), SOURCE_DIAMETER,
|
|
39
|
-
0, null, true, Infinity, 0, ANGLE_STEP,
|
|
40
|
-
{ emergentPhysics: true, minRadius: 2 });
|
|
41
|
-
|
|
42
|
-
// ── SHUT-OFF — the bang is one burst era, then the field evolves on its own ──
|
|
43
|
-
var t = 0;
|
|
44
|
-
exp.addTickFunction(function () {
|
|
45
|
-
t++;
|
|
46
|
-
if (t >= EMIT_TICKS) { source.setActive(false); }
|
|
47
|
-
return [];
|
|
48
|
-
});
|
|
49
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SCENE — Blackbody
|
|
5
|
-
*
|
|
6
|
-
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world, the
|
|
7
|
-
* oven, its thermostat and the radiation sink are all constructed here; the sugar objects
|
|
8
|
-
* (OvenBox, LargeObject), the renderer and the engine are imports. Registered by
|
|
9
|
-
* lib/shim/registry/walkthrough.js (rung blackbody).
|
|
10
|
-
*
|
|
11
|
-
* Physics (zero new — composition of goldened behaviours): a cavity of absorbing walls holds a
|
|
12
|
-
* population of leaves; the OvenBox thermostat keeps the cavity's quanta near the TEMPERATURE
|
|
13
|
-
* target by emitting, and colliding leaves merge into parent frames (bench H2a–c,
|
|
14
|
-
* collide → merge → promote; `withInterQuantumCollisions`). A parent's fullness IS its frequency
|
|
15
|
-
* (wavelength = capacity/mass, bench M4), so what leaks out of the slit in the right wall is a
|
|
16
|
-
* spectrum of frame sizes — the emergent Planck-like curve — with no energy quanta assumed.
|
|
17
|
-
* Everything that escapes is absorbed by the sink at the right edge (the experiment boundary).
|
|
18
|
-
*
|
|
19
|
-
* Order matters (it seeds the run): the world, the brownian/collision flags, the quantum
|
|
20
|
-
* layer, the thermostat SLOT (reserved before the oven so the oven's own tick-fns run after it),
|
|
21
|
-
* the oven, the thermostat, the sink. Same calls, same order, same seed ⇒ the same RNG stream.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { CollisionActions, CollisionTypes, FrqtlControlOverlay, FrqtlControlsPanel, LargeObject, Rectangle } from '@descix/frqtl-web/parts';
|
|
25
|
-
import * as ControlRegistry from '@descix/frqtl-web/shim/controls/registry.js';
|
|
26
|
-
|
|
27
|
-
const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
|
|
28
|
-
const SINK_DEPTH = 30; // the absorbing strip at the right canvas edge (px)
|
|
29
|
-
|
|
30
|
-
export const parts = {
|
|
31
|
-
// ── THE SINK — the experiment boundary on the slit side ─────────────────
|
|
32
|
-
radiation_sink: [LargeObject, {
|
|
33
|
-
rect: { left: W - SINK_DEPTH, top: 0, w: SINK_DEPTH, h: H },
|
|
34
|
-
collision_type: CollisionTypes.NONE, actions: [CollisionActions.ABSORB],
|
|
35
|
-
reposition: 'closest', color: [0, 0, 0]
|
|
36
|
-
}],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export default function buildExperiment(exp) {
|
|
40
|
-
var TEMPERATURE = 1500; // target quanta inside the cavity (the knob below)
|
|
41
|
-
var QUANTUM_DIM = 3; // the layer leaves promote into: an 8 px box edge, 64 cells of capacity in 2D
|
|
42
|
-
var BOX_FRACTION = 1 / 3; // the cavity's outer box, as a fraction of the canvas
|
|
43
|
-
var WALL = 10; // wall thickness (px)
|
|
44
|
-
var SLIT_HEIGHT = 60; // the radiation slit in the right wall (px)
|
|
45
|
-
|
|
46
|
-
// ── WORLD — a 2D canvas + universe, fundamental layer 0 (1x1 leaves), λ multiplier 1 ──
|
|
47
|
-
exp.createCanvas(W, H, { dimension: 0, wavelengthScale: 1 });
|
|
48
|
-
var u = exp.universe;
|
|
49
|
-
u.brownianMotion = true;
|
|
50
|
-
u.withInterQuantumCollisions = true;
|
|
51
|
-
u.addDimension(QUANTUM_DIM);
|
|
52
|
-
|
|
53
|
-
// ── THE OVEN — the cavity, centred on the canvas ────────────────────────
|
|
54
|
-
var outer = new Rectangle(Math.round(W / 2), Math.round(H / 2),
|
|
55
|
-
Math.round(W * BOX_FRACTION), Math.round(H * BOX_FRACTION), null, true);
|
|
56
|
-
// The thermostat runs FIRST each tick: reserve its slot before the oven registers its own.
|
|
57
|
-
var thermostatSlot = u.experimentTickFunctions.length;
|
|
58
|
-
u.experimentTickFunctions.push(null);
|
|
59
|
-
var oven = exp.addObject('OvenBox', {
|
|
60
|
-
name: 'oven',
|
|
61
|
-
left: outer.left + WALL, top: outer.top + WALL,
|
|
62
|
-
width: outer.w - 2 * WALL, height: outer.h - 2 * WALL,
|
|
63
|
-
wallThickness: WALL, temperature: TEMPERATURE,
|
|
64
|
-
slitHeight: SLIT_HEIGHT, // the slit is in the RIGHT wall (OvenBox's own slit side)
|
|
65
|
-
color: [120, 120, 120]
|
|
66
|
-
});
|
|
67
|
-
u.experimentTickFunctions[thermostatSlot] = oven.getTemperatureTickFunction();
|
|
68
|
-
|
|
69
|
-
exp.declare(parts); // the sink (parts.radiation_sink)
|
|
70
|
-
|
|
71
|
-
// ── THE KNOB — temperature, live (the oven's own thermostat target) ─────
|
|
72
|
-
var registry = ControlRegistry.makeControlRegistry();
|
|
73
|
-
registry.registerControls('blackbody', [
|
|
74
|
-
{ kind: 'scalar', id: 'temperature', label: 'temperature (quanta)', min: 50, max: 8000, step: 50,
|
|
75
|
-
value: TEMPERATURE, fmt: function (v) { return String(v); },
|
|
76
|
-
onChange: function (v) { oven.setTemperature(v); },
|
|
77
|
-
place: { x: 0.02, y: 0.03 } }
|
|
78
|
-
]);
|
|
79
|
-
// Chrome (a page only): expose the knobs to a host frame, and mount the overlay through the
|
|
80
|
-
// runner's ONE door (runner.controlHost) — before a canvas is mounted (a notebook pane) there is
|
|
81
|
-
// none, so the knobs stay in the exposed registry only.
|
|
82
|
-
FrqtlControlsPanel.exposeRegistry(registry, { runner: exp.runner });
|
|
83
|
-
if (exp.runner.controlHost) { FrqtlControlOverlay.mount(exp.runner, { registry: registry, name: 'blackbody' }); }
|
|
84
|
-
}
|