@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
package/dist/frqtl/scenes/sun.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
|
|
7
7
|
* (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
|
|
8
|
-
* the parts (`exp.addObject`) and the render intent are all here; the sugar
|
|
8
|
+
* the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
|
|
9
9
|
* objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
|
|
10
10
|
* (rung r8-sun).
|
|
11
11
|
*/
|
|
@@ -21,18 +21,21 @@
|
|
|
21
21
|
|
|
22
22
|
import { CameraControls, SphereEmitter } from '@descix/frqtl-web/parts';
|
|
23
23
|
|
|
24
|
+
export const parts = {
|
|
25
|
+
source: [SphereEmitter, {
|
|
26
|
+
preset: 'spherical', emissionBoxSide: 0, is2D: false,
|
|
27
|
+
chargeMode: 'random', fundamentalDimension: 2,
|
|
28
|
+
wavelength: 1, fundamentalWaveLength: 64, emitEveryTicks: 1
|
|
29
|
+
}],
|
|
30
|
+
};
|
|
31
|
+
|
|
24
32
|
export default function buildExperiment(exp) {
|
|
25
33
|
// Rung 8 (Sun) — the sun.png panel regime: a 3D omni POINT source, virtual mass 16
|
|
26
34
|
// (fullness 1) via wavelength:1, λ scale 16 via fundamentalWaveLength:64, emit every 1.
|
|
27
35
|
// The sphere shape + god-rays + glow FX ride the renderState (three-only capabilities).
|
|
28
36
|
// The sun's yellow is the NATURAL fullness->color (mass=capacity, fullness 1); no forced color.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
chargeMode: 'random', fundamentalDimension: 2,
|
|
32
|
-
wavelength: 1, fundamentalWaveLength: 64, emitEveryTicks: 1
|
|
33
|
-
};
|
|
34
|
-
exp.createScene(600, 400, 600, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
|
|
35
|
-
exp.addObject('SphereEmitter', SRC);
|
|
37
|
+
exp.createScene(600, 400, 600, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
|
|
38
|
+
exp.declare(parts);
|
|
36
39
|
if (typeof document !== 'undefined' && exp.runner) {
|
|
37
40
|
if (exp.runner.controlHost) { CameraControls.attach(exp.runner, exp.runner.controlHost); }
|
|
38
41
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
|
|
7
7
|
* (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
|
|
8
|
-
* the parts (`exp.addObject`) and the render intent are all here; the sugar
|
|
8
|
+
* the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
|
|
9
9
|
* objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
|
|
10
10
|
* (rungs r6a-two-source, r6b-two-fountains, r6c-plasma-fusion).
|
|
11
11
|
*/
|
|
@@ -16,22 +16,27 @@
|
|
|
16
16
|
|
|
17
17
|
import { SphereEmitter } from '@descix/frqtl-web/parts';
|
|
18
18
|
|
|
19
|
+
// The classic self-interference config called TWICE, centered ±32px from canvas
|
|
20
|
+
// center (64px apart = 1×fundamentalWaveLength, i.e. λ_eff/4 since λ_eff = 4×fWL).
|
|
21
|
+
// The world is init'd ONCE, then TWO parts are constructed on it — no flag, no
|
|
22
|
+
// re-init, no stacking. The
|
|
23
|
+
// lattice BETWEEN the two visible sources is EMERGENT density superposition (SU1).
|
|
24
|
+
const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
|
|
25
|
+
const cx = Math.floor(W / 2), cy = Math.floor(H / 2);
|
|
26
|
+
const CFG = {
|
|
27
|
+
is2D: true, preset: 'spherical',
|
|
28
|
+
emissionShell: 'surface', innerR: 16,
|
|
29
|
+
fundamentalDimension: 2, wavelength: 32,
|
|
30
|
+
chargeMode: 'random', fundamentalWaveLength: 64,
|
|
31
|
+
burstSize: 72, emitEveryTicks: 1
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const parts = {
|
|
35
|
+
left: [SphereEmitter, { ...CFG, center: [cx - 32, cy] }],
|
|
36
|
+
right: [SphereEmitter, { ...CFG, center: [cx + 32, cy] }],
|
|
37
|
+
};
|
|
38
|
+
|
|
19
39
|
export default function buildExperiment(exp) {
|
|
20
|
-
// The classic self-interference config called TWICE, centered ±32px from canvas
|
|
21
|
-
// center (64px apart = 1×fundamentalWaveLength, i.e. λ_eff/4 since λ_eff = 4×fWL).
|
|
22
|
-
// The world is init'd ONCE, then TWO parts are constructed on it — no flag, no
|
|
23
|
-
// re-init, no stacking. The
|
|
24
|
-
// lattice BETWEEN the two visible sources is EMERGENT density superposition (SU1).
|
|
25
|
-
var W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
|
|
26
|
-
var cx = Math.floor(W / 2), cy = Math.floor(H / 2);
|
|
27
|
-
var CFG = {
|
|
28
|
-
is2D: true, preset: 'spherical',
|
|
29
|
-
emissionShell: 'surface', innerR: 16,
|
|
30
|
-
fundamentalDimension: 2, wavelength: 32,
|
|
31
|
-
chargeMode: 'random', fundamentalWaveLength: 64,
|
|
32
|
-
burstSize: 72, emitEveryTicks: 1
|
|
33
|
-
};
|
|
34
40
|
exp.createCanvas(W, H, SphereEmitter.worldCfg(CFG)); // THE WORLD: a 2D canvas + universe — first, then parts
|
|
35
|
-
exp.
|
|
36
|
-
exp.addObject('SphereEmitter', Object.assign({}, CFG, { center: [cx + 32, cy] }));
|
|
41
|
+
exp.declare(parts);
|
|
37
42
|
}
|
|
@@ -5,22 +5,25 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
|
|
7
7
|
* (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
|
|
8
|
-
* the parts (`exp.addObject`) and the render intent are all here; the sugar
|
|
8
|
+
* the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
|
|
9
9
|
* objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
|
|
10
10
|
* (rung r2-wave).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { SphereEmitter } from '@descix/frqtl-web/parts';
|
|
14
14
|
|
|
15
|
+
export const parts = {
|
|
16
|
+
source: [SphereEmitter, {
|
|
17
|
+
preset: 'particle-walk', is2D: true,
|
|
18
|
+
fundamentalDimension: 2, wavelength: 8
|
|
19
|
+
}],
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
export default function buildExperiment(exp) {
|
|
16
23
|
// Rung 2 — Rung 1 + raise the leaf dimension to 2 (WAVELENGTH_DEMO_DIMENSION):
|
|
17
24
|
// wavelength is a NO-OP at dim 0 (capacity=1 saturates mass), so raising the
|
|
18
25
|
// fundamental gives the wavelength arg leverage. wavelength:8 -> mass 2 ->
|
|
19
26
|
// transverse period grows from ~16px (Rung 1) to a visible ~128px snake.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
fundamentalDimension: 2, wavelength: 8
|
|
23
|
-
};
|
|
24
|
-
exp.createCanvas(600, 400, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
|
|
25
|
-
exp.addObject('SphereEmitter', SRC);
|
|
27
|
+
exp.createCanvas(600, 400, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
|
|
28
|
+
exp.declare(parts);
|
|
26
29
|
}
|
|
@@ -1,121 +1,137 @@
|
|
|
1
1
|
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* lib/shim/bundle/index.js —
|
|
5
|
-
* The canonical owner of the bundle format
|
|
6
|
-
* (design §2): a folder that zips for download —
|
|
4
|
+
* lib/shim/bundle/index.js — Export and Import of a project folder.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* index.html ← standalone host (@descix/frqtl-web bootProject over the served folder)
|
|
11
|
-
* manifest.json ← Module envelope minus `source` + bundle-only fields (E1)
|
|
12
|
-
* preview.png ← optional golden screenshot
|
|
6
|
+
* A project is a folder (the stored row's owner is
|
|
7
|
+
* lib/shim/storage/folders.js). The Export IS the folder, zipped, plus the host page that boots it:
|
|
13
8
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
9
|
+
* exportFolder(files, renderState, { sdkVersion }) → zipBytes
|
|
10
|
+
* the zip holds EXACTLY the folder's files plus a generated `index.html` (indexHtml.js: bootProject over the
|
|
11
|
+
* served folder, frqtl.json naming the entry, the boot bag inlined by its one writer).
|
|
12
|
+
* importArchive(bytes, fileName) → { files, renderState } (a normalized folder row)
|
|
13
|
+
* a zip → its files minus `index.html`, whose boot bag (indexHtml.js readBootBag) becomes renderState;
|
|
14
|
+
* it must carry frqtl.json at its root (an archive with no index.html imports with
|
|
15
|
+
* renderState null)
|
|
16
|
+
* one `.js` → the one-file project { 'sketch.js': text, 'frqtl.json': manifestFor({ name: <basename>,
|
|
17
|
+
* entry: 'sketch.js' }) }, renderState null — a 3D sketch then fails loud at boot on the
|
|
18
|
+
* renderer (the existing named refusal); nothing guesses a display
|
|
19
|
+
* REFUSED, naming the migration: a zip carrying `experiment.js` or `manifest.json`, or a JSON v4 module
|
|
20
|
+
* envelope — the retired export format. REFUSED by rule: a one-file `.mjs` (frqtl.json's entry rule admits a
|
|
21
|
+
* `.js` entry only), anything else.
|
|
19
22
|
*
|
|
20
|
-
*
|
|
23
|
+
* PURE + DOM-free (Uint8Array in and out). The browser download and file read are lib/features/ide/bundle-export.js.
|
|
24
|
+
* zip.js (the STORE zip) is imported by the Record too; its API is frozen. Layer 2 (physics): none.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
'use strict';
|
|
24
28
|
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { normalizeSavedRenderState } from '../render-state/index.js';
|
|
29
|
+
import { zipStore, unzipStore, textBytes } from './zip.js';
|
|
30
|
+
import { buildIndexHtml, readBootBag } from './indexHtml.js';
|
|
31
|
+
import { normalizeFolderRow, EXPORT_PAGE } from '../storage/folders.js';
|
|
32
|
+
import { MANIFEST_FILE, MANIFEST_KEYS, parseManifest, serializeManifest, manifestFor } from '../project/manifest.js';
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
export var
|
|
33
|
-
export var FILE_EXPERIMENT_FRQNB = 'experiment.frqnb';
|
|
34
|
-
export var FILE_INDEX_HTML = 'index.html';
|
|
35
|
-
export var FILE_MANIFEST_JSON = 'manifest.json';
|
|
36
|
-
export var FILE_PREVIEW_PNG = 'preview.png';
|
|
34
|
+
/** The files of the retired export format (the v4 envelope's bundle). */
|
|
35
|
+
export var RETIRED_ARCHIVE_FILES = Object.freeze(['experiment.js', 'manifest.json']);
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
* Conceptually the dispatch's `(source, envelopeMeta, renderState, previewPngBytes,
|
|
42
|
-
* sdkVersion)`: here `source` + `renderState` + `envelopeMeta` arrive together AS
|
|
43
|
-
* the Module envelope (the exact shape STUDIO's Save path already builds), so the
|
|
44
|
-
* bundle↔envelope round-trip is a clean identity. `manifest` ferries the envelope
|
|
45
|
-
* minus `source` (E1) — no field re-enumeration.
|
|
46
|
-
*
|
|
47
|
-
* @param {object} envelope — a `lib/shim/storage/modules.js` Module envelope
|
|
48
|
-
* (`{id,name,source,renderState,…}`; `source` = the recipe, REQUIRED).
|
|
49
|
-
* @param {object} opts
|
|
50
|
-
* @param {string} opts.sdkVersion — REQUIRED CDN pin (derived, never a literal).
|
|
51
|
-
* @param {Uint8Array|null} [opts.previewPngBytes] — preview image bytes, or null.
|
|
52
|
-
* @param {string} [opts.stage='600x400'] — canvas stage for the .frqnb projection.
|
|
53
|
-
* @param {string} [opts.prose] — optional teaching blurb for the .frqnb markdown.
|
|
54
|
-
* @returns {{files: {name:string, bytes:Uint8Array}[], zipBytes: Uint8Array}}
|
|
55
|
-
*/
|
|
56
|
-
export function assembleBundle(envelope, opts) {
|
|
57
|
-
if (!envelope || typeof envelope !== 'object') {
|
|
58
|
-
throw new Error('[bundle] assembleBundle(envelope): envelope must be an object.');
|
|
59
|
-
}
|
|
60
|
-
if (typeof envelope.source !== 'string' || envelope.source.length === 0) {
|
|
61
|
-
throw new Error('[bundle] assembleBundle: envelope.source (the recipe) is required.');
|
|
62
|
-
}
|
|
63
|
-
opts = opts || {};
|
|
64
|
-
var preview = (opts.previewPngBytes instanceof Uint8Array && opts.previewPngBytes.length > 0)
|
|
65
|
-
? opts.previewPngBytes : null;
|
|
37
|
+
var RETIRED = 'this is the retired export format; the project folder replaced it (frqtl.json + sketch.js)';
|
|
38
|
+
var SKETCH = 'sketch.js';
|
|
66
39
|
|
|
67
|
-
|
|
68
|
-
sdkVersion: opts.sdkVersion,
|
|
69
|
-
preview: preview ? FILE_PREVIEW_PNG : null
|
|
70
|
-
});
|
|
40
|
+
var _strict = new TextDecoder('utf-8', { fatal: true });
|
|
71
41
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
var files = [
|
|
75
|
-
{ name: FILE_EXPERIMENT_JS, bytes: textBytes(envelope.source) },
|
|
76
|
-
{ name: FILE_EXPERIMENT_FRQNB, bytes: textBytes(buildFrqnb({
|
|
77
|
-
name: envelope.name, source: envelope.source,
|
|
78
|
-
renderState: normalizedRs, stage: opts.stage, prose: opts.prose
|
|
79
|
-
})) },
|
|
80
|
-
{ name: FILE_INDEX_HTML, bytes: textBytes(buildIndexHtml({
|
|
81
|
-
name: envelope.name, sdkVersion: opts.sdkVersion, renderState: normalizedRs,
|
|
82
|
-
scripts: Array.isArray(envelope.scripts) ? envelope.scripts : [] // the envelope's setup-factory delta
|
|
83
|
-
})) },
|
|
84
|
-
{ name: FILE_MANIFEST_JSON, bytes: textBytes(JSON.stringify(manifest, null, 2)) }
|
|
85
|
-
];
|
|
86
|
-
if (preview) { files.push({ name: FILE_PREVIEW_PNG, bytes: preview }); }
|
|
87
|
-
|
|
88
|
-
return { files: files, zipBytes: zipStore(files) };
|
|
42
|
+
function _fail(msg) {
|
|
43
|
+
throw new Error('[bundle] ' + msg + ' (FAIL-LOUD.)');
|
|
89
44
|
}
|
|
90
45
|
|
|
91
46
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* @
|
|
96
|
-
* @returns {{envelope: object, previewPngBytes: Uint8Array|null}}
|
|
47
|
+
* @param {Object<string,string>} files — the folder (frqtl.json + its files)
|
|
48
|
+
* @param {object|null} renderState — the host's boot bag (normalized by its owner through the folder row)
|
|
49
|
+
* @param {{sdkVersion: string}} opts — the `@descix/frqtl-web` pin the page imports (derived, never a literal)
|
|
50
|
+
* @returns {Uint8Array} the zip
|
|
97
51
|
*/
|
|
98
|
-
export function
|
|
99
|
-
var
|
|
100
|
-
var
|
|
101
|
-
|
|
52
|
+
export function exportFolder(files, renderState, opts) {
|
|
53
|
+
var row = normalizeFolderRow({ files: files, renderState: renderState });
|
|
54
|
+
var sdkVersion = opts && opts.sdkVersion;
|
|
55
|
+
var name = parseManifest(row.files[MANIFEST_FILE]).name;
|
|
56
|
+
var entries = Object.keys(row.files).map(function (p) { return { name: p, bytes: textBytes(row.files[p]) }; });
|
|
57
|
+
entries.push({ name: EXPORT_PAGE, bytes: textBytes(buildIndexHtml({ name: name, sdkVersion: sdkVersion, renderState: row.renderState })) });
|
|
58
|
+
return zipStore(entries);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _isZip(bytes) {
|
|
62
|
+
return bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b &&
|
|
63
|
+
((bytes[2] === 0x03 && bytes[3] === 0x04) || (bytes[2] === 0x05 && bytes[3] === 0x06));
|
|
64
|
+
}
|
|
102
65
|
|
|
103
|
-
|
|
104
|
-
|
|
66
|
+
function _text(bytes, what) {
|
|
67
|
+
try { return _strict.decode(bytes); }
|
|
68
|
+
catch (e) { _fail(what + ' is not UTF-8 text — a project folder holds text files only.'); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _baseName(fileName) {
|
|
72
|
+
var base = String(fileName).split(/[\\/]/).pop();
|
|
73
|
+
return base;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _fromZip(bytes) {
|
|
77
|
+
var entries = unzipStore(bytes).filter(function (e) { return !/\/$/.test(e.name); });
|
|
78
|
+
var names = entries.map(function (e) { return e.name; });
|
|
79
|
+
var retired = RETIRED_ARCHIVE_FILES.filter(function (f) { return names.indexOf(f) !== -1; });
|
|
80
|
+
if (retired.length) {
|
|
81
|
+
_fail('the archive carries ' + retired.join(' and ') + ': ' + RETIRED + '.');
|
|
105
82
|
}
|
|
106
|
-
if (
|
|
107
|
-
|
|
83
|
+
if (names.indexOf(MANIFEST_FILE) === -1) {
|
|
84
|
+
_fail('the archive carries no ' + MANIFEST_FILE + ' at its root — a project archive is the folder zipped ' +
|
|
85
|
+
'(frqtl.json + sketch.js + …), as an Export writes it.');
|
|
108
86
|
}
|
|
87
|
+
var files = {};
|
|
88
|
+
var renderState = null;
|
|
89
|
+
entries.forEach(function (e) {
|
|
90
|
+
var text = _text(e.bytes, '"' + e.name + '"');
|
|
91
|
+
if (e.name === EXPORT_PAGE) { renderState = readBootBag(text); return; }
|
|
92
|
+
files[e.name] = text;
|
|
93
|
+
});
|
|
94
|
+
return normalizeFolderRow({ files: files, renderState: renderState });
|
|
95
|
+
}
|
|
109
96
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
97
|
+
function _fromJson(text) {
|
|
98
|
+
var raw;
|
|
99
|
+
try { raw = JSON.parse(text); }
|
|
100
|
+
catch (e) { _fail('the file is neither a project archive (.zip) nor a sketch (.js), and not valid JSON either.'); }
|
|
101
|
+
if (raw && typeof raw === 'object' && ('schemaVersion' in raw || 'scripts' in raw) && 'source' in raw) {
|
|
102
|
+
_fail('the file is a v4 module envelope: ' + RETIRED + '.');
|
|
115
103
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
104
|
+
_fail('the JSON file is not a project — Import takes a project archive (.zip) or one sketch (.js).');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _fromSketch(text, fileName) {
|
|
108
|
+
var base = _baseName(fileName);
|
|
109
|
+
var name = base.replace(/\.js$/, '');
|
|
110
|
+
if (!name) { _fail('the sketch file "' + base + '" has no name before ".js".'); }
|
|
111
|
+
var files = {};
|
|
112
|
+
files[MANIFEST_FILE] = serializeManifest(manifestFor({ name: name, entry: SKETCH }));
|
|
113
|
+
files[SKETCH] = text;
|
|
114
|
+
return normalizeFolderRow({ files: files, renderState: null });
|
|
115
|
+
}
|
|
119
116
|
|
|
120
|
-
|
|
117
|
+
/**
|
|
118
|
+
* @param {Uint8Array} bytes
|
|
119
|
+
* @param {string} fileName — the imported file's name (a one-file sketch's basename is its project name)
|
|
120
|
+
* @returns {Readonly<{files: Object<string,string>, renderState: object}>}
|
|
121
|
+
*/
|
|
122
|
+
export function importArchive(bytes, fileName) {
|
|
123
|
+
if (!(bytes instanceof Uint8Array)) { _fail('importArchive(bytes, fileName): bytes is a Uint8Array.'); }
|
|
124
|
+
if (typeof fileName !== 'string' || fileName.length === 0) { _fail('importArchive(bytes, fileName): the file\'s name.'); }
|
|
125
|
+
if (_isZip(bytes)) { return _fromZip(bytes); }
|
|
126
|
+
var base = _baseName(fileName);
|
|
127
|
+
if (/\.mjs$/i.test(base)) {
|
|
128
|
+
var entryRule = MANIFEST_KEYS.filter(function (r) { return r.key === 'entry'; })[0].rule;
|
|
129
|
+
_fail('"' + base + '" is a .mjs module — a one-file Import takes a .js sketch, because a project\'s entry is ' +
|
|
130
|
+
entryRule + ' (frqtl.json, lib/shim/project/manifest.js MANIFEST_KEYS). Rename it to .js and import it again.');
|
|
131
|
+
}
|
|
132
|
+
var text = _text(bytes, '"' + base + '"');
|
|
133
|
+
if (/\.js$/.test(base)) { return _fromSketch(text, base); }
|
|
134
|
+
if (/\.json$/i.test(base) || /^\s*\{/.test(text)) { return _fromJson(text); }
|
|
135
|
+
_fail('"' + base + '" is not a project — Import takes a project archive (.zip, as an Export writes it) or one ' +
|
|
136
|
+
'sketch (.js).');
|
|
121
137
|
}
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* lib/shim/bundle/indexHtml.js — the
|
|
5
|
-
*
|
|
4
|
+
* lib/shim/bundle/indexHtml.js — the generated `index.html` of an exported project folder: the HOST PAGE,
|
|
5
|
+
* and the ONE writer + ONE parser of the boot bag it carries.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* the page's import map carries those rows and the scene doors' row, rendered by the ONE table
|
|
11
|
-
* (`pageImportMapFor(<the pinned dist root>/)`, lib/shim/project/specifiers.js), so the sketch resolves
|
|
12
|
-
* to the SAME module URLs the boot loads. The pinned dist root is the router's one spelling
|
|
13
|
-
* (`cdnDistBaseFor`); the `@<pin>` is the passed-in `sdkVersion` (DERIVED at export time — never a
|
|
14
|
-
* literal here).
|
|
7
|
+
* buildIndexHtml({ name, sdkVersion, renderState }) → the page text
|
|
8
|
+
* readBootBag(html) → the renderState it carries (the import's one reader)
|
|
9
|
+
* BOOT_BAG_ID 'frqtl-boot' — the bag's element id
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* The page boots the folder it sits in as a SERVED PROJECT: `bootProject(mount, { url: './' }, { renderState })`
|
|
12
|
+
* from `@descix/frqtl-web@<pin>` on the CDN. `frqtl.json` names the entry (P2a: the linker reads it first), so
|
|
13
|
+
* the page names no file. The import map is the ONE table's (`pageImportMapFor(<the pinned dist root>/)`,
|
|
14
|
+
* lib/shim/project/specifiers.js), so a sketch's `@descix/frqtl-web/...` imports resolve to the same module URLs
|
|
15
|
+
* the boot loads. The pinned dist root is the router's one spelling (`cdnDistBaseFor`); the pin is the passed-in
|
|
16
|
+
* `sdkVersion` (derived at export time, never a literal here).
|
|
17
|
+
*
|
|
18
|
+
* The boot bag — the host's renderState, which never enters frqtl.json or the project identity — is embedded as
|
|
19
|
+
* `<script type="application/json" id="frqtl-boot">`, `<` escaped so no payload closes the element. The page reads
|
|
20
|
+
* it from that element; an Import reads it back with readBootBag and drops the page. A page opened via `file://`
|
|
21
|
+
* cannot fetch the folder: it says to serve the folder over http.
|
|
19
22
|
*
|
|
20
23
|
* Layer 2 (physics): none — pure host projection.
|
|
21
24
|
*/
|
|
@@ -25,41 +28,38 @@
|
|
|
25
28
|
import { pageImportMapFor, FRQTL_WEB_BOOT } from '../project/specifiers.js';
|
|
26
29
|
import { cdnDistBaseFor } from '../engine/router.js';
|
|
27
30
|
|
|
31
|
+
/** The boot bag's element id. */
|
|
32
|
+
export var BOOT_BAG_ID = 'frqtl-boot';
|
|
33
|
+
|
|
34
|
+
var _BAG_OPEN = '<script type="application/json" id="' + BOOT_BAG_ID + '">';
|
|
35
|
+
var _BAG_CLOSE = '</script>';
|
|
36
|
+
|
|
28
37
|
function _escapeHtml(s) {
|
|
29
38
|
return String(s == null ? '' : s)
|
|
30
39
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
/** Safe inline
|
|
42
|
+
/** Safe inline JSON: `<` escaped so a `</script>`-like payload cannot break out. */
|
|
34
43
|
function _inlineJson(obj) {
|
|
35
44
|
return JSON.stringify(obj == null ? null : obj).replace(/</g, '\\u003c');
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
* Build the standalone `index.html` for a recipe bundle.
|
|
40
|
-
*
|
|
41
48
|
* @param {object} args
|
|
42
|
-
* @param {string} args.name — project name (page title)
|
|
43
|
-
* @param {string} args.sdkVersion — the `@descix/frqtl-web@<pin>` version (REQUIRED)
|
|
44
|
-
* @param {object}
|
|
45
|
-
*
|
|
46
|
-
* @param {string[]} [args.scripts] — the v4 envelope's setup-factory delta (inlined as JSON): nothing
|
|
47
|
-
* classic is left to load, so the page REFUSES a non-empty list at boot,
|
|
48
|
-
* naming the fix (import the factory in the sketch).
|
|
49
|
-
* @param {string} [args.recipeFile='experiment.js'] — the project's entry file.
|
|
50
|
-
* @returns {string} the complete `index.html`.
|
|
49
|
+
* @param {string} args.name — the project's name (frqtl.json `name`; the page title)
|
|
50
|
+
* @param {string} args.sdkVersion — the `@descix/frqtl-web@<pin>` version (REQUIRED)
|
|
51
|
+
* @param {object|null} args.renderState — the normalized boot bag (null boots with the page's defaults)
|
|
52
|
+
* @returns {string} the complete `index.html`
|
|
51
53
|
*/
|
|
52
54
|
export function buildIndexHtml(args) {
|
|
53
55
|
args = args || {};
|
|
54
56
|
if (typeof args.sdkVersion !== 'string' || args.sdkVersion.length === 0) {
|
|
55
|
-
throw new Error('[bundle/indexHtml] buildIndexHtml: args.sdkVersion is REQUIRED (the CDN pin).');
|
|
57
|
+
throw new Error('[bundle/indexHtml] buildIndexHtml: args.sdkVersion is REQUIRED (the CDN pin). (FAIL-LOUD.)');
|
|
58
|
+
}
|
|
59
|
+
if (typeof args.name !== 'string' || args.name.length === 0) {
|
|
60
|
+
throw new Error('[bundle/indexHtml] buildIndexHtml: args.name is the project\'s frqtl.json name. (FAIL-LOUD.)');
|
|
56
61
|
}
|
|
57
|
-
var name = (typeof args.name === 'string' && args.name.length > 0) ? args.name : 'FRQTL Recipe';
|
|
58
|
-
var recipeFile = (typeof args.recipeFile === 'string' && args.recipeFile.length > 0)
|
|
59
|
-
? args.recipeFile : 'experiment.js';
|
|
60
62
|
var importMapJson = _inlineJson(pageImportMapFor(cdnDistBaseFor(args.sdkVersion) + '/'));
|
|
61
|
-
var renderStateJson = _inlineJson(args.renderState || null);
|
|
62
|
-
var scriptsJson = _inlineJson(Array.isArray(args.scripts) ? args.scripts : []);
|
|
63
63
|
|
|
64
64
|
return [
|
|
65
65
|
'<!DOCTYPE html>',
|
|
@@ -67,7 +67,7 @@ export function buildIndexHtml(args) {
|
|
|
67
67
|
'<head>',
|
|
68
68
|
' <meta charset="UTF-8">',
|
|
69
69
|
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
70
|
-
' <title>' + _escapeHtml(name) + ' — FRQTL</title>',
|
|
70
|
+
' <title>' + _escapeHtml(args.name) + ' — FRQTL</title>',
|
|
71
71
|
' <style>',
|
|
72
72
|
' html,body{margin:0;height:100%;background:#0b0b0f;color:#ddd;',
|
|
73
73
|
' font:14px/1.5 ui-sans-serif,system-ui,sans-serif;}',
|
|
@@ -80,23 +80,21 @@ export function buildIndexHtml(args) {
|
|
|
80
80
|
'<body>',
|
|
81
81
|
' <div id="frqtl-mount"></div>',
|
|
82
82
|
' <pre id="frqtl-err"></pre>',
|
|
83
|
+
' ' + _BAG_OPEN + _inlineJson(args.renderState) + _BAG_CLOSE,
|
|
83
84
|
' <script type="importmap">' + importMapJson + '</script>',
|
|
84
85
|
' <script type="module">',
|
|
85
86
|
' import { bootProject } from ' + JSON.stringify(FRQTL_WEB_BOOT.specifier) + ';',
|
|
86
|
-
' const
|
|
87
|
-
' const SCRIPTS = ' + scriptsJson + ';',
|
|
87
|
+
' const renderState = JSON.parse(document.getElementById(' + JSON.stringify(BOOT_BAG_ID) + ').textContent);',
|
|
88
88
|
' const mount = document.getElementById("frqtl-mount");',
|
|
89
89
|
' const errBox = document.getElementById("frqtl-err");',
|
|
90
90
|
' function fail(title, detail){',
|
|
91
91
|
' errBox.style.display = "block";',
|
|
92
92
|
' errBox.textContent = title + "\\n\\n" + detail;',
|
|
93
93
|
' }',
|
|
94
|
-
' if (
|
|
95
|
-
' fail("
|
|
96
|
-
' } else if (location.protocol === "file:") {',
|
|
97
|
-
' fail("Serve this folder over http", "e.g. `npx serve` — a file:// page cannot fetch ' + _escapeHtml(recipeFile) + '.");',
|
|
94
|
+
' if (location.protocol === "file:") {',
|
|
95
|
+
' fail("Serve this folder over http", "e.g. `npx serve` — a file:// page cannot fetch the project files.");',
|
|
98
96
|
' } else {',
|
|
99
|
-
' bootProject(mount, { url: new URL("./", location.href).href
|
|
97
|
+
' bootProject(mount, { url: new URL("./", location.href).href }, { renderState: renderState })',
|
|
100
98
|
' .catch((e) => fail("FRQTL project boot failed", String(e && e.stack || e)));',
|
|
101
99
|
' }',
|
|
102
100
|
' </script>',
|
|
@@ -105,3 +103,32 @@ export function buildIndexHtml(args) {
|
|
|
105
103
|
''
|
|
106
104
|
].join('\n');
|
|
107
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* THE parser of the boot bag: the renderState an exported page carries (null when it carries none).
|
|
109
|
+
* THROWS when the page holds no bag element, or more than one, or one that is not JSON.
|
|
110
|
+
* @param {string} html
|
|
111
|
+
* @returns {object|null}
|
|
112
|
+
*/
|
|
113
|
+
export function readBootBag(html) {
|
|
114
|
+
if (typeof html !== 'string') {
|
|
115
|
+
throw new Error('[bundle/indexHtml] readBootBag(html): the page text (got ' + typeof html + '). (FAIL-LOUD.)');
|
|
116
|
+
}
|
|
117
|
+
var at = html.indexOf(_BAG_OPEN);
|
|
118
|
+
if (at === -1) {
|
|
119
|
+
throw new Error('[bundle/indexHtml] the index.html carries no boot bag (<script type="application/json" id="' +
|
|
120
|
+
BOOT_BAG_ID + '">) — it is not the page an Export writes. (FAIL-LOUD.)');
|
|
121
|
+
}
|
|
122
|
+
if (html.indexOf(_BAG_OPEN, at + 1) !== -1) {
|
|
123
|
+
throw new Error('[bundle/indexHtml] the index.html carries two boot bags — an Export writes one. (FAIL-LOUD.)');
|
|
124
|
+
}
|
|
125
|
+
var start = at + _BAG_OPEN.length;
|
|
126
|
+
var end = html.indexOf(_BAG_CLOSE, start);
|
|
127
|
+
if (end === -1) {
|
|
128
|
+
throw new Error('[bundle/indexHtml] the boot bag element is not closed. (FAIL-LOUD.)');
|
|
129
|
+
}
|
|
130
|
+
try { return JSON.parse(html.slice(start, end)); }
|
|
131
|
+
catch (e) {
|
|
132
|
+
throw new Error('[bundle/indexHtml] the boot bag is not valid JSON: ' + ((e && e.message) || e) + '. (FAIL-LOUD.)');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -148,7 +148,7 @@ var DIST_PROFILE = buildDistProfile('../..');
|
|
|
148
148
|
// lib/shim/engine/test/router.test.mjs asserts them against the two
|
|
149
149
|
// package.json#version fields so a forgotten bump fails `make verify`, not a
|
|
150
150
|
// silent stale-CDN-URL bug.
|
|
151
|
-
var CDN_SDK_VERSION = '0.6.0-
|
|
151
|
+
var CDN_SDK_VERSION = '0.6.0-p4.1'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
|
|
152
152
|
var CDN_MATH_SDK_VERSION = '0.3.2'; // sdk/egpt-math-sdk/package.json#version — bound by router.test.mjs; bumped with each npm publish
|
|
153
153
|
|
|
154
154
|
/**
|
|
@@ -507,7 +507,7 @@ var _assertProfileParity = (function () {
|
|
|
507
507
|
* // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
|
|
508
508
|
* const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
|
|
509
509
|
* console.log(cdnRouter.resolve('compat/engine.js'));
|
|
510
|
-
* // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.0-
|
|
510
|
+
* // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.0-p4.1/dist/frqtl/compat/engine.js'
|
|
511
511
|
*
|
|
512
512
|
* // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
|
|
513
513
|
* const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });
|
package/dist/shim/index.js
CHANGED
|
@@ -22,7 +22,7 @@ export { WALKTHROUGH, walkthroughIs3D, walkthroughModes, walkthroughGroup,
|
|
|
22
22
|
walkthroughById,
|
|
23
23
|
FIRST_CONTACT_WALKTHROUGH_ID,
|
|
24
24
|
defaultWalkthroughModeId, effectiveRenderState } from './registry/walkthrough.js';
|
|
25
|
-
export {
|
|
25
|
+
export { projectOf, scenePath, EMPTY_SCENE } from './registry/scene-source.js';
|
|
26
26
|
// The host-side experiment-iframe guard (the false-200 trap). Exported from the
|
|
27
27
|
// barrel so every host that frames an experiment page (lib/features/ide's GYM)
|
|
28
28
|
// asserts against the SAME contract.
|
|
@@ -99,7 +99,7 @@ export const PROSE_CELL_TYPES = Object.freeze(['markdown']);
|
|
|
99
99
|
* e.g. `setupDoubleSlit`). Absent → `'factory'`, so every existing notebook is
|
|
100
100
|
* byte-identical.
|
|
101
101
|
* - `'recipe'`: the module's DEFAULT export is a `buildExperiment(exp)` RECIPE (a single
|
|
102
|
-
* RecipeContext arg — the sketch form
|
|
102
|
+
* RecipeContext arg — the sketch form the scene files
|
|
103
103
|
* write). The shim builds a
|
|
104
104
|
* `frqtl.RecipeContext` and runs `exp.build(buildExperiment)` (the canonical
|
|
105
105
|
* co-init call + `addObject()` recipe path), NOT the factory signature.
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Thin CRUD helpers that persist a serialized <frqtl-notebook> document on top
|
|
7
7
|
* of any StorageProvider (lib/shim/storage/index.js) using the "notebooks"
|
|
8
|
-
* namespace
|
|
9
|
-
* lib/shim/storage/
|
|
10
|
-
* projects, Studio modules, and Notebooks.
|
|
8
|
+
* namespace, through the shared namespace-CRUD factory
|
|
9
|
+
* (lib/shim/storage/make-namespace-store.js).
|
|
11
10
|
*
|
|
12
11
|
* ── Why this lives in lib/shim ──────────────────────────────────────────────
|
|
13
12
|
*
|
|
@@ -185,8 +185,8 @@ var HOST_TOKENS = Object.freeze([
|
|
|
185
185
|
why: 'chrome is a HOST-COMPOSITION decision (whether the experiment\'s own ' +
|
|
186
186
|
'control chrome DOM renders), never a per-experiment physics/render ' +
|
|
187
187
|
'parameter; forwarding it from an outer host URL into an embedded ' +
|
|
188
|
-
'iframe would reintroduce the hidden-experiment-chrome bug
|
|
189
|
-
'
|
|
188
|
+
'iframe would reintroduce the hidden-experiment-chrome bug (the GYM ' +
|
|
189
|
+
'must always show the ' +
|
|
190
190
|
'experiment\'s own full chrome, never chrome=canvas — see the ' +
|
|
191
191
|
'"WHEN should a host set chrome=canvas" doc above for the general ' +
|
|
192
192
|
'criterion, not just the GYM\'s specific answer to it)'
|