@descix/frqtl-node 0.6.0-p1.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 +6 -4
- package/dist/dist-manifest.json +71 -65
- package/dist/eyes/index.js +5 -2
- 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 +33 -6
- package/dist/frqtl/objects/RecipeContext.js +284 -13
- 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 +216 -29
- package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
- package/dist/frqtl/objects/record/Record.js +346 -0
- package/dist/frqtl/objects/record/open-record.js +22 -0
- 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/FrqtlVision.js +21 -6
- 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 +118 -96
- package/dist/shim/bundle/indexHtml.js +66 -39
- 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 +3 -5
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/kernel.js +1 -1
- package/dist/shim/notebook/setup-builder.js +2 -2
- package/dist/shim/notebook/store.js +2 -3
- 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/index.js +16 -3
- package/dist/shim/project/link.js +224 -46
- package/dist/shim/project/manifest.js +151 -0
- package/dist/shim/project/parts-writer.js +68 -7
- package/dist/shim/project/read-text.js +43 -0
- package/dist/shim/project/record-format.js +345 -0
- package/dist/shim/project/specifiers.js +104 -15
- package/dist/shim/registry/exhibits.js +15 -34
- package/dist/shim/registry/index.js +3 -3
- package/dist/shim/registry/scene-roster.js +35 -0
- package/dist/shim/registry/scene-source.js +48 -64
- package/dist/shim/registry/walkthrough.js +6 -6
- 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/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 +4 -3
- package/package.json +19 -1
- package/dist/shim/bundle/frqnb.js +0 -112
- package/dist/shim/bundle/manifest.js +0 -95
- package/dist/shim/registry/experiments.js +0 -215
- package/dist/shim/storage/modules.js +0 -249
- package/dist/shim/storage/projects.js +0 -150
|
@@ -19,102 +19,119 @@
|
|
|
19
19
|
// captured descriptor DATA) keeps the population bounded so FPS stays healthy, Restart-safe.
|
|
20
20
|
//
|
|
21
21
|
// ONE MODE-AWARE SOURCE. The mode strip selects the RENDERER the host boots
|
|
22
|
-
// (renderState.renderer), and this scene reads it ONCE,
|
|
22
|
+
// (renderState.renderer), and this scene reads it ONCE, inside buildExperiment: `three` ⇒
|
|
23
23
|
// exp.createScene (a 600x400x600 world), else exp.createCanvas (600x400). The ring geometry,
|
|
24
|
-
// tilts, colours, laser and gate are the SAME in both modes (2D≡3D)
|
|
25
|
-
//
|
|
24
|
+
// tilts, colours, laser and gate are the SAME in both modes (2D≡3D). `parts` is written in the
|
|
25
|
+
// 3D default mode, and flat2D (below) derives the 2D block from it — the scene's own statement of
|
|
26
|
+
// two of its deltas:
|
|
26
27
|
// (a) every mirror is a 3D SLAB — a z-slab centred on the beam plane z=0, rendered as a plate;
|
|
27
28
|
// (b) the laser is VOLUMETRIC (is2D:false — it braids in y AND z; the slab depth catches the
|
|
28
|
-
// z-braid; its tilted xy faces reflect by the 2θ law, its near/far z-faces bounce vz)
|
|
29
|
+
// z-braid; its tilted xy faces reflect by the 2θ law, its near/far z-faces bounce vz).
|
|
30
|
+
// `IS3D` guards the rest, in code after exp.declare:
|
|
29
31
|
// (c) two invisible destroy z-collimators flush with the slab z-band;
|
|
30
|
-
// (d) the population counter carries a z-slab covering the WHOLE universe z-band
|
|
32
|
+
// (d) the population counter carries a z-slab covering the WHOLE universe z-band (the emitter's
|
|
33
|
+
// gateCounter names it, so it stays a code declaration);
|
|
31
34
|
// (e) MOVE/LOOK camera joysticks (the OMNI_3D pattern).
|
|
32
35
|
// The 3D mode's render intent — god-rays at the sun's settings, computed from the light
|
|
33
36
|
// particles only (godraySource 'leaf'), and the 'table-top' camera — rides the registry mode's
|
|
34
37
|
// renderState (lib/shim/registry/walkthrough.js), whose god-ray VALUES are the sun's, owned by
|
|
35
38
|
// lib/shim/render/sun-look.js.
|
|
36
39
|
|
|
37
|
-
import { CameraControls, SphereEmitter } from '@descix/frqtl-web/parts';
|
|
40
|
+
import { CameraControls, SphereEmitter, Wall } from '@descix/frqtl-web/parts';
|
|
38
41
|
import * as FrqtlDisplay from '@descix/frqtl-web/compat/renderer/display-slot.js';
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
const W = 600, H = 400, WORLD_D = 600; // THE WORLD this scene states (its co-init call below)
|
|
44
|
+
const f = Math.min(W, H) / 600;
|
|
45
|
+
// R 225·f: adjacent decagon vertices sit 2R·sin18° ≈ 0.618R apart vs slab
|
|
46
|
+
// height ~110·f, so the mirrors separate with clear gaps. Fit: R + rotated-slab half-diagonal
|
|
47
|
+
// (≈55·f) stays inside min(W,H)/2 with margin.
|
|
48
|
+
const CX = Math.round(W / 2), CY = Math.round(H / 2), R = Math.round(225 * f), HOP = 3, ROT = 12;
|
|
49
|
+
// CAP 6400 / burst 8 (brightness retune): a VERY BRIGHT braid. The cap is
|
|
50
|
+
// LIVE-controllable off the population counter (select 'population' in Layers → Inspector
|
|
51
|
+
// 'Population Gate').
|
|
52
|
+
const SW = Math.max(10, Math.round(14 * f)), SH = Math.max(70, Math.round(110 * f)), CAP = 6400;
|
|
53
|
+
// SD — the 3D slab z-depth: a 2:1 tile (twice the slab height, z-centred on the beam plane).
|
|
54
|
+
// The scene z-collimator clamps ALL quanta to this band, so every mirror provably faces every
|
|
55
|
+
// alive quantum.
|
|
56
|
+
const SD = 2 * SH;
|
|
57
|
+
// Each mirror a DIFFERENT colour = its identity (a red→magenta rainbow); the recolor hook
|
|
58
|
+
// stamps that colour onto every particle it reflects (string_map['render.color']).
|
|
59
|
+
const PAL = [[255,60,60],[255,140,0],[255,220,0],[150,230,40],[40,210,90],[0,210,190],[0,170,255],[70,90,255],[150,70,235],[235,50,190]];
|
|
60
|
+
// Hit-order vertices of the {10/3} star (m1..m10 named IN the order the beam visits them).
|
|
61
|
+
const order = []; for (var k = 0; k < 10; k++) { order.push((k * HOP) % 10); }
|
|
62
|
+
const D2R = Math.PI / 180;
|
|
63
|
+
const vpos = function (v) { var a = (v * 36 + ROT) * D2R; return [CX + R * Math.cos(a), CY + R * Math.sin(a)]; };
|
|
64
|
+
const pos = order.map(vpos);
|
|
65
|
+
const deg = function (a) { return ((a * 180 / Math.PI) % 360 + 360) % 360; };
|
|
66
|
+
const angOf = function (p, q) { return deg(Math.atan2(q[1] - p[1], q[0] - p[0])); };
|
|
67
|
+
// Per-mirror IN-PLANE orientation from the in/out chords: ori = (in+out−180)/2, branch chosen
|
|
68
|
+
// so the engine reflect law out = 180 + 2·ori − in reproduces the outgoing chord exactly.
|
|
69
|
+
// Orientation is z-axis rotation, so the 3D slab depth adds NO new angle.
|
|
70
|
+
const ORI = [];
|
|
71
|
+
for (var j = 0; j < 10; j++) {
|
|
72
|
+
var pr = pos[(j + 9) % 10], cu = pos[j], nx = pos[(j + 1) % 10];
|
|
73
|
+
var inA = angOf(pr, cu), outA = angOf(cu, nx);
|
|
74
|
+
var o = (((inA + outA - 180) / 2) % 360 + 360) % 360;
|
|
75
|
+
var op = ((180 + 2 * o - inA) % 360 + 360) % 360;
|
|
76
|
+
if (Math.abs(((op - outA + 540) % 360) - 180) > 0.5) { o = (o + 90) % 360; }
|
|
77
|
+
ORI.push(o);
|
|
78
|
+
}
|
|
79
|
+
// The NARROW STRAIGHT-WALK laser (the exemplar): the SAME emitter as Rung 1
|
|
80
|
+
// (Straight Walk) — preset 'particle-walk', every walk-character field INHERITED from the same
|
|
81
|
+
// factory defaults (chargeMode 'positive', dim 0 = 'light' P=1, fundamentalWaveLength 64 → the
|
|
82
|
+
// λ16 braided transverse ripple) — with ONLY the deliberate loop deltas: (a) a 2px source line
|
|
83
|
+
// (emissionBoxSide 2 vs r1's default 10), (b) seated on the m10→m1 leg JUST CLEAR of m10 (70·f
|
|
84
|
+
// past its vertex > the rotated-slab half-diagonal ≈55·f) and aimed along the leg (the longest
|
|
85
|
+
// clear throw: leg 1.618R ≈ 364·f), (c) a burst of 8/tick, and (d) the DECLARED emission gate.
|
|
86
|
+
const pS = pos[9], p0 = pos[0];
|
|
87
|
+
const dx = p0[0] - pS[0], dy = p0[1] - pS[1], L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L;
|
|
88
|
+
const emit = [Math.round(pS[0] + 70 * f * ux), Math.round(pS[1] + 70 * f * uy)];
|
|
89
|
+
// EMISSION-CAP GATE as DECLARED DATA (the blackbody-thermostat pattern):
|
|
90
|
+
// emissionCap + gateCounter compile at CONSTRUCTION into a host tick-fn reading
|
|
91
|
+
// Wall.getLiveCount on the background counter named 'population' (placed below — the by-name
|
|
92
|
+
// reference resolves lazily on the first tick). The closed loop has NO sink, so the gate pauses
|
|
93
|
+
// emission at the cap and resumes below it: the population plateaus. Restart-safe: the parts
|
|
94
|
+
// writer keeps the two fields in the entry and the rebuilt emitter recompiles the gate (no free recipe tick-fn).
|
|
95
|
+
// is2D: false (the 3D default) opens activeAxes.z — the volumetric laser; flat2D writes it true.
|
|
96
|
+
// The parts, written in the 3D default mode (the mode strip's default): the laser, then the ten mirrors at the
|
|
97
|
+
// star vertices — each its own colour + the recolor tag, each a 3D SLAB (z-slab -SD/2..SD/2). 2D is derived from
|
|
98
|
+
// this block by flat2D below.
|
|
99
|
+
export const parts = {
|
|
100
|
+
source: [SphereEmitter, { preset: 'particle-walk', is2D: false, center: emit, orientationDeg: angOf(pS, p0), emissionBoxSide: 2, burstSize: 8, emissionCap: CAP, gateCounter: 'population' }],
|
|
101
|
+
m1: [Wall, { rect: [Math.round(pos[0][0] - SW / 2), Math.round(pos[0][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[0], color: PAL[0], hooks: ['recolor'] }],
|
|
102
|
+
m2: [Wall, { rect: [Math.round(pos[1][0] - SW / 2), Math.round(pos[1][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[1], color: PAL[1], hooks: ['recolor'] }],
|
|
103
|
+
m3: [Wall, { rect: [Math.round(pos[2][0] - SW / 2), Math.round(pos[2][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[2], color: PAL[2], hooks: ['recolor'] }],
|
|
104
|
+
m4: [Wall, { rect: [Math.round(pos[3][0] - SW / 2), Math.round(pos[3][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[3], color: PAL[3], hooks: ['recolor'] }],
|
|
105
|
+
m5: [Wall, { rect: [Math.round(pos[4][0] - SW / 2), Math.round(pos[4][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[4], color: PAL[4], hooks: ['recolor'] }],
|
|
106
|
+
m6: [Wall, { rect: [Math.round(pos[5][0] - SW / 2), Math.round(pos[5][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[5], color: PAL[5], hooks: ['recolor'] }],
|
|
107
|
+
m7: [Wall, { rect: [Math.round(pos[6][0] - SW / 2), Math.round(pos[6][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[6], color: PAL[6], hooks: ['recolor'] }],
|
|
108
|
+
m8: [Wall, { rect: [Math.round(pos[7][0] - SW / 2), Math.round(pos[7][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[7], color: PAL[7], hooks: ['recolor'] }],
|
|
109
|
+
m9: [Wall, { rect: [Math.round(pos[8][0] - SW / 2), Math.round(pos[8][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[8], color: PAL[8], hooks: ['recolor'] }],
|
|
110
|
+
m10: [Wall, { rect: [Math.round(pos[9][0] - SW / 2), Math.round(pos[9][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[9], color: PAL[9], hooks: ['recolor'] }],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** This scene's 2D delta, stated once: the same keys in the same order, each props copied with its z-slab
|
|
114
|
+
* (z, depth) dropped and is2D written true where it is spelled. Pure; adds no key. */
|
|
115
|
+
function flat2D(parts) {
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const key of Object.keys(parts)) {
|
|
118
|
+
const props = Object.assign({}, parts[key][1]);
|
|
119
|
+
delete props.z;
|
|
120
|
+
delete props.depth;
|
|
121
|
+
if ('is2D' in props) { props.is2D = true; }
|
|
122
|
+
out[key] = [parts[key][0], props];
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
41
126
|
|
|
42
127
|
export default function buildExperiment(exp) {
|
|
43
128
|
// The booted display (the host's renderState.renderer) decides which world this mode makes.
|
|
44
129
|
var IS3D = FrqtlDisplay.name() === 'three';
|
|
45
|
-
var
|
|
46
|
-
var f = Math.min(W, H) / 600;
|
|
47
|
-
// R 225·f: adjacent decagon vertices sit 2R·sin18° ≈ 0.618R apart vs slab
|
|
48
|
-
// height ~110·f, so the mirrors separate with clear gaps. Fit: R + rotated-slab half-diagonal
|
|
49
|
-
// (≈55·f) stays inside min(W,H)/2 with margin.
|
|
50
|
-
var CX = Math.round(W / 2), CY = Math.round(H / 2), R = Math.round(225 * f), HOP = 3, ROT = 12;
|
|
51
|
-
// CAP 6400 / burst 8 (brightness retune): a VERY BRIGHT braid. The cap is
|
|
52
|
-
// LIVE-controllable off the population counter (select 'population' in Layers → Inspector
|
|
53
|
-
// 'Population Gate').
|
|
54
|
-
var SW = Math.max(10, Math.round(14 * f)), SH = Math.max(70, Math.round(110 * f)), CAP = 6400;
|
|
55
|
-
// SD — the 3D slab z-depth: a 2:1 tile (twice the slab height, z-centred on the beam plane).
|
|
56
|
-
// The scene z-collimator clamps ALL quanta to this band, so every mirror provably faces every
|
|
57
|
-
// alive quantum.
|
|
58
|
-
var SD = 2 * SH;
|
|
59
|
-
// Each mirror a DIFFERENT colour = its identity (a red→magenta rainbow); the recolor hook
|
|
60
|
-
// stamps that colour onto every particle it reflects (string_map['render.color']).
|
|
61
|
-
var PAL = [[255,60,60],[255,140,0],[255,220,0],[150,230,40],[40,210,90],[0,210,190],[0,170,255],[70,90,255],[150,70,235],[235,50,190]];
|
|
62
|
-
// Hit-order vertices of the {10/3} star (m1..m10 named IN the order the beam visits them).
|
|
63
|
-
var order = []; for (var k = 0; k < 10; k++) { order.push((k * HOP) % 10); }
|
|
64
|
-
var D2R = Math.PI / 180;
|
|
65
|
-
var vpos = function (v) { var a = (v * 36 + ROT) * D2R; return [CX + R * Math.cos(a), CY + R * Math.sin(a)]; };
|
|
66
|
-
var pos = order.map(vpos);
|
|
67
|
-
var deg = function (a) { return ((a * 180 / Math.PI) % 360 + 360) % 360; };
|
|
68
|
-
var angOf = function (p, q) { return deg(Math.atan2(q[1] - p[1], q[0] - p[0])); };
|
|
69
|
-
// Per-mirror IN-PLANE orientation from the in/out chords: ori = (in+out−180)/2, branch chosen
|
|
70
|
-
// so the engine reflect law out = 180 + 2·ori − in reproduces the outgoing chord exactly.
|
|
71
|
-
// Orientation is z-axis rotation, so the 3D slab depth adds NO new angle.
|
|
72
|
-
var ORI = [];
|
|
73
|
-
for (var j = 0; j < 10; j++) {
|
|
74
|
-
var pr = pos[(j + 9) % 10], cu = pos[j], nx = pos[(j + 1) % 10];
|
|
75
|
-
var inA = angOf(pr, cu), outA = angOf(cu, nx);
|
|
76
|
-
var o = (((inA + outA - 180) / 2) % 360 + 360) % 360;
|
|
77
|
-
var op = ((180 + 2 * o - inA) % 360 + 360) % 360;
|
|
78
|
-
if (Math.abs(((op - outA + 540) % 360) - 180) > 0.5) { o = (o + 90) % 360; }
|
|
79
|
-
ORI.push(o);
|
|
80
|
-
}
|
|
81
|
-
// The NARROW STRAIGHT-WALK laser (the exemplar): the SAME emitter as Rung 1
|
|
82
|
-
// (Straight Walk) — preset 'particle-walk', every walk-character field INHERITED from the same
|
|
83
|
-
// factory defaults (chargeMode 'positive', dim 0 = 'light' P=1, fundamentalWaveLength 64 → the
|
|
84
|
-
// λ16 braided transverse ripple) — with ONLY the deliberate loop deltas: (a) a 2px source line
|
|
85
|
-
// (emissionBoxSide 2 vs r1's default 10), (b) seated on the m10→m1 leg JUST CLEAR of m10 (70·f
|
|
86
|
-
// past its vertex > the rotated-slab half-diagonal ≈55·f) and aimed along the leg (the longest
|
|
87
|
-
// clear throw: leg 1.618R ≈ 364·f), (c) a burst of 8/tick, and (d) the DECLARED emission gate.
|
|
88
|
-
var pS = pos[9], p0 = pos[0];
|
|
89
|
-
var dx = p0[0] - pS[0], dy = p0[1] - pS[1], L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L;
|
|
90
|
-
var emit = [Math.round(pS[0] + 70 * f * ux), Math.round(pS[1] + 70 * f * uy)];
|
|
91
|
-
// EMISSION-CAP GATE as DECLARED DATA (the blackbody-thermostat pattern):
|
|
92
|
-
// emissionCap + gateCounter compile at CONSTRUCTION into a host tick-fn reading
|
|
93
|
-
// Wall.getLiveCount on the background counter named 'population' (placed below — the by-name
|
|
94
|
-
// reference resolves lazily on the first tick). The closed loop has NO sink, so the gate pauses
|
|
95
|
-
// emission at the cap and resumes below it: the population plateaus. Restart-safe: codegen
|
|
96
|
-
// captures the two fields and the rebuilt emitter recompiles the gate (no free recipe tick-fn).
|
|
97
|
-
// is2D:!IS3D opens activeAxes.z in 3D — the volumetric laser.
|
|
98
|
-
var SRC = {
|
|
99
|
-
preset: 'particle-walk', is2D: !IS3D, center: emit, orientationDeg: angOf(pS, p0),
|
|
100
|
-
emissionBoxSide: 2, burstSize: 8,
|
|
101
|
-
emissionCap: CAP, gateCounter: 'population'
|
|
102
|
-
};
|
|
130
|
+
var P = IS3D ? parts : flat2D(parts);
|
|
103
131
|
// THE WORLD — first, then parts: a 3D scene + universe (600x400x600) or a 2D canvas + universe.
|
|
104
|
-
if (IS3D) { exp.createScene(W, H, WORLD_D, SphereEmitter.worldCfg(
|
|
105
|
-
else { exp.createCanvas(W, H, SphereEmitter.worldCfg(
|
|
106
|
-
exp.declare(
|
|
107
|
-
exp.addObject('SphereEmitter', SRC);
|
|
108
|
-
// The ten mirrors at the star vertices — each its own colour + the recolor tag. In 3D each is a
|
|
109
|
-
// SLAB (z-slab −SD/2..SD/2); in 2D the bag is EMPTY, so the wall is the flat mirror.
|
|
110
|
-
var ZSLAB = IS3D ? { z: -Math.round(SD / 2), depth: SD } : {};
|
|
111
|
-
for (var i = 0; i < 10; i++) {
|
|
112
|
-
var p = pos[i];
|
|
113
|
-
exp.addObject('Wall', Object.assign(
|
|
114
|
-
{ rect: [Math.round(p[0] - SW / 2), Math.round(p[1] - SH / 2), SW, SH] }, ZSLAB,
|
|
115
|
-
{ collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[i],
|
|
116
|
-
name: 'm' + (i + 1), color: PAL[i], hooks: ['recolor'] }));
|
|
117
|
-
}
|
|
132
|
+
if (IS3D) { exp.createScene(W, H, WORLD_D, SphereEmitter.worldCfg(P.source[1])); }
|
|
133
|
+
else { exp.createCanvas(W, H, SphereEmitter.worldCfg(P.source[1])); }
|
|
134
|
+
exp.declare(P);
|
|
118
135
|
if (IS3D) {
|
|
119
136
|
// SCENE Z-COLLIMATOR (without it the z lets things pass through instead of
|
|
120
137
|
// reflecting): the legs' z-braid diffuses lap over lap; any quantum beyond the mirrors'
|
|
@@ -1,121 +1,143 @@
|
|
|
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
|
+
var tops = names.map(function (n) { return n.split('/')[0]; }).filter(function (t, i, a) { return a.indexOf(t) === i; });
|
|
85
|
+
if (tops.length === 1 && names.indexOf(tops[0] + '/' + MANIFEST_FILE) !== -1 &&
|
|
86
|
+
names.every(function (n) { return n.indexOf(tops[0] + '/') === 0; })) {
|
|
87
|
+
_fail('the archive carries no ' + MANIFEST_FILE + ' at its root — the archive holds the folder `' + tops[0] + '/` rather ' +
|
|
88
|
+
'than its contents — zip the folder\'s CONTENTS (' + MANIFEST_FILE + ' at the root), as an Export writes it.');
|
|
89
|
+
}
|
|
90
|
+
_fail('the archive carries no ' + MANIFEST_FILE + ' at its root — a project archive is the folder zipped ' +
|
|
91
|
+
'(frqtl.json + sketch.js + …), as an Export writes it.');
|
|
108
92
|
}
|
|
93
|
+
var files = {};
|
|
94
|
+
var renderState = null;
|
|
95
|
+
entries.forEach(function (e) {
|
|
96
|
+
var text = _text(e.bytes, '"' + e.name + '"');
|
|
97
|
+
if (e.name === EXPORT_PAGE) { renderState = readBootBag(text); return; }
|
|
98
|
+
files[e.name] = text;
|
|
99
|
+
});
|
|
100
|
+
return normalizeFolderRow({ files: files, renderState: renderState });
|
|
101
|
+
}
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
function _fromJson(text) {
|
|
104
|
+
var raw;
|
|
105
|
+
try { raw = JSON.parse(text); }
|
|
106
|
+
catch (e) { _fail('the file is neither a project archive (.zip) nor a sketch (.js), and not valid JSON either.'); }
|
|
107
|
+
if (raw && typeof raw === 'object' && ('schemaVersion' in raw || 'scripts' in raw) && 'source' in raw) {
|
|
108
|
+
_fail('the file is a v4 module envelope: ' + RETIRED + '.');
|
|
115
109
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
_fail('the JSON file is not a project — Import takes a project archive (.zip) or one sketch (.js).');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _fromSketch(text, fileName) {
|
|
114
|
+
var base = _baseName(fileName);
|
|
115
|
+
var name = base.replace(/\.js$/, '');
|
|
116
|
+
if (!name) { _fail('the sketch file "' + base + '" has no name before ".js".'); }
|
|
117
|
+
var files = {};
|
|
118
|
+
files[MANIFEST_FILE] = serializeManifest(manifestFor({ name: name, entry: SKETCH }));
|
|
119
|
+
files[SKETCH] = text;
|
|
120
|
+
return normalizeFolderRow({ files: files, renderState: null });
|
|
121
|
+
}
|
|
119
122
|
|
|
120
|
-
|
|
123
|
+
/**
|
|
124
|
+
* @param {Uint8Array} bytes
|
|
125
|
+
* @param {string} fileName — the imported file's name (a one-file sketch's basename is its project name)
|
|
126
|
+
* @returns {Readonly<{files: Object<string,string>, renderState: object}>}
|
|
127
|
+
*/
|
|
128
|
+
export function importArchive(bytes, fileName) {
|
|
129
|
+
if (!(bytes instanceof Uint8Array)) { _fail('importArchive(bytes, fileName): bytes is a Uint8Array.'); }
|
|
130
|
+
if (typeof fileName !== 'string' || fileName.length === 0) { _fail('importArchive(bytes, fileName): the file\'s name.'); }
|
|
131
|
+
if (_isZip(bytes)) { return _fromZip(bytes); }
|
|
132
|
+
var base = _baseName(fileName);
|
|
133
|
+
if (/\.mjs$/i.test(base)) {
|
|
134
|
+
var entryRule = MANIFEST_KEYS.filter(function (r) { return r.key === 'entry'; })[0].rule;
|
|
135
|
+
_fail('"' + base + '" is a .mjs module — a one-file Import takes a .js sketch, because a project\'s entry is ' +
|
|
136
|
+
entryRule + ' (frqtl.json, lib/shim/project/manifest.js MANIFEST_KEYS). Rename it to .js and import it again.');
|
|
137
|
+
}
|
|
138
|
+
var text = _text(bytes, '"' + base + '"');
|
|
139
|
+
if (/\.js$/.test(base)) { return _fromSketch(text, base); }
|
|
140
|
+
if (/\.json$/i.test(base) || /^\s*\{/.test(text)) { return _fromJson(text); }
|
|
141
|
+
_fail('"' + base + '" is not a project — Import takes a project archive (.zip, as an Export writes it) or one ' +
|
|
142
|
+
'sketch (.js).');
|
|
121
143
|
}
|
|
@@ -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
|
+
}
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
// action — a MOMENTARY verb: {id,label,action} where `action` is a member of
|
|
102
102
|
// the closed ACTIONS vocabulary below. Stateless (no `value`, no
|
|
103
103
|
// read-back) and serializable by NAME, so a declared array of them
|
|
104
|
-
// round-trips through
|
|
104
|
+
// round-trips through the scene's text with no new machinery. Actions are
|
|
105
105
|
// deliberately NOT expressed as a `bind` — a bind is a property
|
|
106
106
|
// read/write pair, and `get()` of "restart" is meaningless.
|
|
107
107
|
//
|