@descix/frqtl-node 0.6.1-p3.1 → 0.6.1-p3.3
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 +1 -1
- package/README.md +2 -2
- package/dist/dist-manifest.json +26 -25
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +301 -12
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +6 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +66 -17
- package/dist/frqtl/objects/FrqtlApp.js +17 -1
- package/dist/frqtl/objects/RecipeContext.js +17 -0
- package/dist/frqtl/objects/record/Record.js +4 -9
- package/dist/frqtl/parts/FrqtlVision.js +158 -27
- package/dist/frqtl/parts/GraphingDetectorWall.js +1 -1
- package/dist/frqtl/parts/LiveChart.js +1 -1
- package/dist/frqtl/parts/SphereEmitter.js +2 -2
- package/dist/frqtl/parts/Tunnel.js +1 -1
- package/dist/frqtl/parts/Wall.js +97 -10
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +33 -22
- package/dist/frqtl/scenes/mirror-route.js +10 -6
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/field-descriptor/index.js +5 -5
- package/dist/shim/host/device.js +42 -0
- package/dist/shim/project/index.js +13 -4
- package/dist/shim/project/parts-spelling.js +8 -1
- package/dist/shim/project/record-format.js +30 -12
- package/dist/shim/registry/exhibits.js +10 -10
- package/dist/shim/registry/studio-landing.js +14 -11
- package/dist/shim/registry/walkthrough.js +38 -90
- package/dist/shim/render/capture-envelope.js +21 -0
- package/dist/shim/render/sun-look.js +6 -11
- package/llms.txt +2 -2
- package/package.json +1 -1
|
@@ -800,7 +800,7 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
|
|
|
800
800
|
'reflects off the two facing mirrors and stays collimated (no random scatter, ' +
|
|
801
801
|
'no leak through the walls). The group is the single author-surface entity; ' +
|
|
802
802
|
'the two mirror Walls it composes carry its `_owner` up-link. Zero physics of its own.',
|
|
803
|
-
// A tunnel has a genuine draggable/resizable extent (like a Wall).
|
|
803
|
+
// A tunnel has a genuine draggable/resizable extent (like a Wall).
|
|
804
804
|
ux: {
|
|
805
805
|
resizable: true,
|
|
806
806
|
draggable: { '2d': true },
|
package/dist/frqtl/parts/Wall.js
CHANGED
|
@@ -774,14 +774,26 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
|
|
|
774
774
|
return out;
|
|
775
775
|
};
|
|
776
776
|
|
|
777
|
+
/** Does the op-program this wall's LargeObject is RUNNING remove the frame it triggers
|
|
778
|
+
* on (`set 0.is_alive = …`)? Read off the program itself — the one statement of what
|
|
779
|
+
* the wall does — never a hand list of behavior names (today: destroy and absorb). On
|
|
780
|
+
* such a wall a frame's listing in `tickData.objects[handle].frames` is not one
|
|
781
|
+
* arrival: the engine collects every frame in the mb_w-padded search rect
|
|
782
|
+
* (ENGINE_CONTRACT §5.1), so a frame is listed alive through its approach ticks and
|
|
783
|
+
* once more on its kill tick. */
|
|
784
|
+
function _runningProgramRemoves(wall) {
|
|
785
|
+
var prog = wall.lo && wall.lo.program;
|
|
786
|
+
return !!(prog && Array.isArray(prog.ops) &&
|
|
787
|
+
prog.ops.some(function (op) { return op && op.set === '0.is_alive'; }));
|
|
788
|
+
}
|
|
789
|
+
|
|
777
790
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
* thermostat's activeQuanta read).
|
|
791
|
+
* ZONE-OCCUPANCY readout (loop-polish B): the number of frames THIS wall's
|
|
792
|
+
* engine collect pass listed on the sampled tick — every frame in its search rect
|
|
793
|
+
* (the rect padded by the dimension's mb_w). For a FULL-AREA background wall
|
|
794
|
+
* (collisionBehavior 'nothing' + the count hook) that listing IS the current live
|
|
795
|
+
* population inside the wall — the ONE canonical "how many live" authority (the
|
|
796
|
+
* emission-cap gate and the LiveChart `liveCount` projector read it).
|
|
785
797
|
*
|
|
786
798
|
* Layer 1 (API): dual-lane, mirroring the LiveChart PROJECTOR_SPEC shape —
|
|
787
799
|
* getLiveCount(tickData) — SAMPLE lane (per tick): read
|
|
@@ -794,12 +806,23 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
|
|
|
794
806
|
* COUNT increment only (bench LO7); counting never moves, kills, or recolors a
|
|
795
807
|
* frame (SU1).
|
|
796
808
|
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
809
|
+
* Guards: THROWS on a REMOVING wall (destroy / absorb) — there the listing reads
|
|
810
|
+
* (1 + approach-halo ticks) x arrivals, never the arrivals; `getArrivals` is the
|
|
811
|
+
* reader. THROWS on a wall without the count hook (no collected-frames buffer → it
|
|
812
|
+
* would silently report 0 forever).
|
|
799
813
|
* @param {Object} [tickData] — the universe.doTick() result (sample lane).
|
|
800
814
|
* @returns {number}
|
|
801
815
|
*/
|
|
802
816
|
Wall.prototype.getLiveCount = function (tickData) {
|
|
817
|
+
if (_runningProgramRemoves(this)) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
'[Wall.getLiveCount] this wall REMOVES what it hits (collisionBehavior "' +
|
|
820
|
+
this.collisionBehavior + '") — getLiveCount is the zone-occupancy reader of a ' +
|
|
821
|
+
'non-removing wall, and on a removing wall it would count each frame once per ' +
|
|
822
|
+
'approach tick plus once on its kill tick. Read arrivals with ' +
|
|
823
|
+
'wall.getArrivals(tickData) → { total, delta } (the engine\'s own per-wall count).'
|
|
824
|
+
);
|
|
825
|
+
}
|
|
803
826
|
var counts = Array.isArray(this.hooks) && this.hooks.indexOf('count') !== -1;
|
|
804
827
|
if (!counts) {
|
|
805
828
|
throw new Error(
|
|
@@ -818,6 +841,70 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
|
|
|
818
841
|
return this._liveCount || 0;
|
|
819
842
|
};
|
|
820
843
|
|
|
844
|
+
/**
|
|
845
|
+
* ARRIVALS readout: how many frames have triggered THIS wall's collision program —
|
|
846
|
+
* read from the engine's own per-wall counter, `tickData.objects[handle].count`, the
|
|
847
|
+
* contract's one read authority for an LO count (ENGINE_CONTRACT §4.3). The `count`
|
|
848
|
+
* hook compiles `set 1.count = 1.count + 1` behind the trigger, so the engine counts
|
|
849
|
+
* one per trigger event; on a removing wall (destroy / absorb) each trigger is the
|
|
850
|
+
* frame's removal, so the count IS the arrivals. Never derived from the collected
|
|
851
|
+
* frame listing (that is the padded search-rect census — see getLiveCount).
|
|
852
|
+
*
|
|
853
|
+
* Layer 1 (API): dual-lane, the same shape as getLiveCount —
|
|
854
|
+
* getArrivals(tickData) — SAMPLE lane: read the engine count and return
|
|
855
|
+
* `{ total, delta }`: `total` = cumulative arrivals since the wall registered,
|
|
856
|
+
* `delta` = the change since the previously sampled TickData (this tick's
|
|
857
|
+
* arrivals when sampled every tick). IDEMPOTENT per TickData — sampling the same
|
|
858
|
+
* TickData twice returns the same `{ total, delta }`.
|
|
859
|
+
* getArrivals() — READ lane: the last sample (`{ total: 0, delta: 0 }` before the
|
|
860
|
+
* first tick).
|
|
861
|
+
*
|
|
862
|
+
* Layer 2 (physics): zero physics — a pure reader of a counter the compiled program
|
|
863
|
+
* already maintains (bench LO7); reading it moves nothing (SU1).
|
|
864
|
+
*
|
|
865
|
+
* Guards: THROWS on a wall without the count hook (the engine counter never moves →
|
|
866
|
+
* it would silently report 0 forever), and on a TickData that is not the one this
|
|
867
|
+
* wall's universe last produced (`universe._lastTickData`, the object `doTick()` returned
|
|
868
|
+
* and every tick listener receives). LO handles restart at 0 in every universe, so a
|
|
869
|
+
* TickData from another world indexes a different wall's count at the same handle; and
|
|
870
|
+
* an earlier TickData of this world would yield a wrong delta.
|
|
871
|
+
* @param {Object} [tickData] — the universe.doTick() result (sample lane).
|
|
872
|
+
* @returns {{ total: number, delta: number }}
|
|
873
|
+
*/
|
|
874
|
+
Wall.prototype.getArrivals = function (tickData) {
|
|
875
|
+
var counts = Array.isArray(this.hooks) && this.hooks.indexOf('count') !== -1;
|
|
876
|
+
if (!counts) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
'[Wall.getArrivals] this wall does not COUNT (collisionBehavior "' +
|
|
879
|
+
this.collisionBehavior + '", hooks ' + JSON.stringify(this.hooks || []) +
|
|
880
|
+
') — the engine increments a wall\'s count only through the `count` hook; ' +
|
|
881
|
+
'add hooks: [\'count\'] (preset \'detector-screen\' = destroy + count).'
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (!this._arrivals) { this._arrivals = { td: null, total: 0, delta: 0 }; }
|
|
885
|
+
var a = this._arrivals;
|
|
886
|
+
if (tickData != null && tickData !== a.td) {
|
|
887
|
+
if (tickData !== this.universe._lastTickData) {
|
|
888
|
+
throw new Error('[Wall.getArrivals] "' + this.name + '": this is not the TickData its ' +
|
|
889
|
+
'own universe last produced (universe._lastTickData) — it is another world\'s, ' +
|
|
890
|
+
'where handle ' + this.lo.handle + ' is a different wall, or an earlier tick of ' +
|
|
891
|
+
'this one. Sample the TickData this wall\'s universe.doTick() just returned. (FAIL-LOUD.)');
|
|
892
|
+
}
|
|
893
|
+
var handle = this.lo.handle;
|
|
894
|
+
var oc = tickData.objects && tickData.objects[handle];
|
|
895
|
+
if (!oc || typeof oc.count !== 'number') {
|
|
896
|
+
throw new Error('[Wall.getArrivals] "' + this.name + '": this world\'s TickData carries no ' +
|
|
897
|
+
'objects[' + handle + '].count — every registered LargeObject is marshalled each ' +
|
|
898
|
+
'tick, so the wall is not registered here. (FAIL-LOUD.)');
|
|
899
|
+
}
|
|
900
|
+
var total = oc.count;
|
|
901
|
+
a.delta = total - a.total;
|
|
902
|
+
a.total = total;
|
|
903
|
+
a.td = tickData;
|
|
904
|
+
}
|
|
905
|
+
return { total: a.total, delta: a.delta };
|
|
906
|
+
};
|
|
907
|
+
|
|
821
908
|
/**
|
|
822
909
|
* ABSORB v2 readout: the wall's live { stored, absorbedTotal, radiatedTotal } (the
|
|
823
910
|
* REAL backing-frame mass ledger). Throws on a non-absorb wall. Call between ticks.
|
|
@@ -884,7 +971,7 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
|
|
|
884
971
|
'on. Zero physics of its own.',
|
|
885
972
|
// STUDIO UX presentation contract (A0 vocab, owned at lib/shim/field-descriptor).
|
|
886
973
|
// A wall has a genuine draggable/resizable extent (unlike the two-radii
|
|
887
|
-
// SphereEmitter) → resizable:true
|
|
974
|
+
// SphereEmitter) → resizable:true (resize zones are 2D; the 3D Hand only moves).
|
|
888
975
|
// shellRender:'run' — a wall is a boundary you SEE during Run (not a Design-only
|
|
889
976
|
// marker like an emitter), so its shell always paints.
|
|
890
977
|
ux: {
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
import { CameraControls, FrqtlControlOverlay, FrqtlControlsPanel, GraphingDetectorWall, SphereEmitter, Tunnel, Wall } from '@descix/frqtl-web/parts';
|
|
45
45
|
import * as ControlRegistry from '@descix/frqtl-web/shim/controls/registry.js';
|
|
46
46
|
import * as FrqtlDisplay from '@descix/frqtl-web/compat/renderer/display-slot.js';
|
|
47
|
-
import * as SunLook from '@descix/frqtl-web/shim/render/sun-look.js';
|
|
48
47
|
|
|
49
48
|
const CW = 600, H = 400, WORLD_D = 600; // THE WORLD this scene states (its co-init call below)
|
|
50
49
|
// GEOMETRY and WAVELENGTH are two INDEPENDENT dials, and they are kept apart on
|
|
@@ -224,33 +223,45 @@ export default function buildExperiment(exp) {
|
|
|
224
223
|
collisionBehavior: 'destroy', name: 'zClipFar', visible: false });
|
|
225
224
|
exp.addObject('Wall', { rect: [0, 0, CW, H], z: -zHalf - 40, depth: 40,
|
|
226
225
|
collisionBehavior: 'destroy', name: 'zClipNear', visible: false });
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
226
|
+
// PACING (wall-clock only, never the ticks): 240 ticks/s, four times the 1:1 lock on a
|
|
227
|
+
// 60 Hz display, so the interference bands build up four times sooner in real time.
|
|
228
|
+
exp.runner.setRateLimits({ tpsLimit: 240 });
|
|
229
|
+
// THE SWITCH: this scene's god-ray look, computed from the light particles only (godraySource
|
|
230
|
+
// 'leaf') with the 'field' pass (each pixel marches along the local direction of travel), and the
|
|
231
|
+
// per-layer glow under it. The numbers are this scene's own, set by eye against a reference
|
|
232
|
+
// STUDIO picture (godrays 2.2 of [0, 3], godrayDensity 1.5 = the top of [0.2, 1.5],
|
|
233
|
+
// glowIntensity 0.66 of [0, 3]); the renderer declares the keys and their ranges and refuses any
|
|
234
|
+
// other. Default ON: the scene writes ON once at build, through the same function the knob calls.
|
|
235
|
+
// OFF restores EXACTLY what was live before the last ON (a host row, a STUDIO slider), through
|
|
236
|
+
// the same declared door. The knob shows its last written value (a render write has no
|
|
237
|
+
// read-back). The picture changes at the next drawn tick, so in a paused (Design) view it shows
|
|
238
|
+
// once the scene runs.
|
|
239
|
+
var RAYS_ON = { godrays: 2.2, godrayDensity: 1.5, godrayMode: 'field', godraySource: 'leaf', glowIntensity: 0.66 };
|
|
240
|
+
var RAYS_DEFAULT = 1;
|
|
241
|
+
var raysPrior = null; // the live values of RAYS_ON's keys captured at ON; written back at OFF
|
|
242
|
+
function setRays(v) {
|
|
243
|
+
if (v) {
|
|
244
|
+
// Capture only on the OFF -> ON edge: an ON pressed while already ON keeps the pre-ON state.
|
|
245
|
+
if (!raysPrior) {
|
|
246
|
+
raysPrior = {};
|
|
247
|
+
Object.keys(RAYS_ON).forEach(function (k) { raysPrior[k] = exp.runner.getRenderOption(k); });
|
|
248
|
+
}
|
|
249
|
+
exp.runner.setRenderOptions(RAYS_ON);
|
|
250
|
+
} else if (raysPrior) {
|
|
251
|
+
exp.runner.setRenderOptions(raysPrior);
|
|
252
|
+
raysPrior = null;
|
|
253
|
+
}
|
|
254
|
+
// OFF with nothing captured: nothing was written by this switch — no write.
|
|
255
|
+
}
|
|
236
256
|
var knobs = ControlRegistry.makeControlRegistry();
|
|
237
257
|
knobs.registerControls('dse-tunnel-rebuild', [
|
|
238
258
|
{ kind: 'scalar', id: 'godrays', label: 'god rays', channel: 'render', widget: 'toggle',
|
|
239
|
-
min: 0, max: 1, step: 1, value:
|
|
259
|
+
min: 0, max: 1, step: 1, value: RAYS_DEFAULT,
|
|
240
260
|
fmt: function (v) { return v ? 'on' : 'off'; },
|
|
241
|
-
onChange:
|
|
242
|
-
if (v) {
|
|
243
|
-
raysPrior = {};
|
|
244
|
-
Object.keys(RAYS_ON).forEach(function (k) { raysPrior[k] = exp.runner.getRenderOption(k); });
|
|
245
|
-
exp.runner.setRenderOptions(RAYS_ON);
|
|
246
|
-
} else if (raysPrior) {
|
|
247
|
-
exp.runner.setRenderOptions(raysPrior);
|
|
248
|
-
raysPrior = null;
|
|
249
|
-
}
|
|
250
|
-
// OFF with nothing captured: the control is at its initial OFF and has written nothing — no write.
|
|
251
|
-
},
|
|
261
|
+
onChange: setRays,
|
|
252
262
|
place: { x: 0.02, y: 0.04 } }
|
|
253
263
|
]);
|
|
264
|
+
setRays(RAYS_DEFAULT);
|
|
254
265
|
// Chrome (a page only): expose the knob to a host frame, and mount the overlay through the
|
|
255
266
|
// runner's ONE door (runner.controlHost) — before a canvas is mounted (a notebook pane) there is
|
|
256
267
|
// none, so the knob stays in the exposed registry only.
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
// (d) the population counter carries a z-slab covering the WHOLE universe z-band (the emitter's
|
|
33
33
|
// gateCounter names it, so it stays a code declaration);
|
|
34
34
|
// (e) MOVE/LOOK camera joysticks (the OMNI_3D pattern).
|
|
35
|
-
// The 3D mode's render intent —
|
|
36
|
-
// particles only (godraySource 'leaf'),
|
|
37
|
-
//
|
|
38
|
-
// lib/shim/
|
|
35
|
+
// The 3D mode's render intent — the particles hidden so the light field alone shows, strong
|
|
36
|
+
// 'field' god-rays computed from the light particles only (godraySource 'leaf'), a soft glow, and
|
|
37
|
+
// a face-on camera close enough that the ring spans the canvas height — rides the registry mode's
|
|
38
|
+
// renderState (lib/shim/registry/walkthrough.js), its one home.
|
|
39
39
|
|
|
40
40
|
import { CameraControls, SphereEmitter, Wall } from '@descix/frqtl-web/parts';
|
|
41
41
|
import * as FrqtlDisplay from '@descix/frqtl-web/compat/renderer/display-slot.js';
|
|
@@ -64,6 +64,9 @@ const vpos = function (v) { var a = (v * 36 + ROT) * D2R; return [CX + R * Math.
|
|
|
64
64
|
const pos = order.map(vpos);
|
|
65
65
|
const deg = function (a) { return ((a * 180 / Math.PI) % 360 + 360) % 360; };
|
|
66
66
|
const angOf = function (p, q) { return deg(Math.atan2(q[1] - p[1], q[0] - p[0])); };
|
|
67
|
+
// Where each mirror belongs: m1..m10 by name, at its star vertex (the centre of the slot in the ring). m4 and m6
|
|
68
|
+
// are declared off theirs below; STUDIO's "try it" reads this to know when a dragged mirror is home.
|
|
69
|
+
export const starPoints = Object.freeze(Object.fromEntries(pos.map(function (p, i) { return ['m' + (i + 1), Object.freeze(p.slice())]; })));
|
|
67
70
|
// Per-mirror IN-PLANE orientation from the in/out chords: ori = (in+out−180)/2, branch chosen
|
|
68
71
|
// so the engine reflect law out = 180 + 2·ori − in reproduces the outgoing chord exactly.
|
|
69
72
|
// Orientation is z-axis rotation, so the 3D slab depth adds NO new angle.
|
|
@@ -101,9 +104,10 @@ export const parts = {
|
|
|
101
104
|
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
105
|
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
106
|
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
|
|
107
|
+
// m4 and m6 stand OFF their star vertices (m4 5 px right, m6 67 px toward the centre): the ring opens broken, so dragging one home is the thing to try.
|
|
108
|
+
m4: [Wall, { rect: [437, 103, SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[3], color: PAL[3], hooks: ['recolor'] }],
|
|
105
109
|
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: [
|
|
110
|
+
m6: [Wall, { rect: [215, 142, SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[5], color: PAL[5], hooks: ['recolor'] }],
|
|
107
111
|
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
112
|
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
113
|
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'] }],
|
|
@@ -152,7 +152,7 @@ var DIST_PROFILE = buildDistProfile('../..');
|
|
|
152
152
|
// lib/shim/engine/test/router.test.mjs asserts them against the two
|
|
153
153
|
// package.json#version fields so a forgotten bump fails `make verify`, not a
|
|
154
154
|
// silent stale-CDN-URL bug.
|
|
155
|
-
var CDN_SDK_VERSION = '0.6.1-p3.
|
|
155
|
+
var CDN_SDK_VERSION = '0.6.1-p3.3'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
|
|
156
156
|
var CDN_MATH_SDK_VERSION = '0.3.3-p2.1'; // sdk/egpt-math-sdk/package.json#version — bound by router.test.mjs; bumped with each npm publish
|
|
157
157
|
|
|
158
158
|
/**
|
|
@@ -526,7 +526,7 @@ var _assertProfileParity = (function () {
|
|
|
526
526
|
* // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
|
|
527
527
|
* const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
|
|
528
528
|
* console.log(cdnRouter.resolve('compat/engine.js'));
|
|
529
|
-
* // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.1-p3.
|
|
529
|
+
* // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.1-p3.3/dist/frqtl/compat/engine.js'
|
|
530
530
|
*
|
|
531
531
|
* // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
|
|
532
532
|
* const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });
|
|
@@ -199,8 +199,8 @@ var UX_HANDLE_ZONES = Object.freeze([
|
|
|
199
199
|
'scale-t', 'scale-b', 'scale-l', 'scale-r'
|
|
200
200
|
]);
|
|
201
201
|
|
|
202
|
-
// The
|
|
203
|
-
//
|
|
202
|
+
// The keys of a ux `draggable` block. Declared data only: STUDIO's Hand drag (2D and 3D) moves
|
|
203
|
+
// any declared object with a position authority and does not read it.
|
|
204
204
|
var UX_DRAGGABLE_MODES = Object.freeze(['2d']);
|
|
205
205
|
|
|
206
206
|
// When a manipulable object's visual shell renders (generalizes the
|
|
@@ -236,8 +236,8 @@ function _uxFail(msg) {
|
|
|
236
236
|
* resizable — boolean (default true). false ⇒ scale handles are never
|
|
237
237
|
* offered even for the SELECTED object (its corners never
|
|
238
238
|
* resize; the first-and-every interaction is select/move).
|
|
239
|
-
* draggable — { '2d':boolean } (default true).
|
|
240
|
-
*
|
|
239
|
+
* draggable — { '2d':boolean } (default true). Declared data only: no STUDIO
|
|
240
|
+
* surface reads it ('3d' is not a key).
|
|
241
241
|
* handles — string[] ⊆ UX_HANDLE_ZONES (default = ALL zones). The
|
|
242
242
|
* live interaction zones; a zone NOT listed is downgraded
|
|
243
243
|
* to 'move' by the overlay (never a resize on the selecting
|
|
@@ -273,7 +273,7 @@ function normalizeUx(partial) {
|
|
|
273
273
|
Object.keys(partial.draggable).forEach(function (k) {
|
|
274
274
|
if (UX_DRAGGABLE_MODES.indexOf(k) === -1) {
|
|
275
275
|
_uxFail('"draggable.' + k + '" is not a draggable mode ([' + UX_DRAGGABLE_MODES.join(', ') +
|
|
276
|
-
'])
|
|
276
|
+
']).');
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
279
|
var dr = { '2d': true };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* device.js — the ONE rule for "this document is open on a phone".
|
|
5
|
+
*
|
|
6
|
+
* MOBILE_DEVICE_QUERY the media query: a coarse primary pointer (a finger, not a mouse) on a viewport no wider
|
|
7
|
+
* than the widest phone held in landscape (932 CSS px, the iPhone Pro Max). A tablet in
|
|
8
|
+
* portrait is narrower than that and counts as a phone; a touch laptop has a fine pointer
|
|
9
|
+
* and does not.
|
|
10
|
+
* isMobileDevice(win) win.matchMedia(MOBILE_DEVICE_QUERY).matches — evaluated in the window that LAYS OUT the
|
|
11
|
+
* page (an app framed by the DeSciX shell measures its own frame). A window without
|
|
12
|
+
* matchMedia THROWS: every browser has it, so its absence is a caller passing the wrong object.
|
|
13
|
+
*
|
|
14
|
+
* Its three consumers ask it once each, at load:
|
|
15
|
+
* - the host-view call site (apps/ide/src/host-view-entry.js) asks the shell for NoChrome on a phone, CodeSite
|
|
16
|
+
* elsewhere;
|
|
17
|
+
* - STUDIO (lib/features/ide/AuthorView.jsx) opens with its side panels collapsed on a phone and puts its Hand tool
|
|
18
|
+
* between the camera joysticks (the phone toolbar Move | Hand | Look), and words its mirror "try it" for that
|
|
19
|
+
* surface (lib/features/ide/studio-try-it.js);
|
|
20
|
+
* - FraqtlIde (lib/features/ide/index.jsx) opens the STUDIO tab card with the controls of that surface
|
|
21
|
+
* (presets.js presetExplainerFor).
|
|
22
|
+
* The static build compiles it into both: the stable host-view entry is bundled as its own self-contained module
|
|
23
|
+
* (apps/ide/vite.config.js frqtl-host-view-entry), the IDE's rollup graph carries its own copy of the same source.
|
|
24
|
+
*
|
|
25
|
+
* Host-neutral ESM with no imports.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** A phone: a coarse primary pointer on a viewport no wider than a phone in landscape. */
|
|
29
|
+
export const MOBILE_DEVICE_QUERY = '(pointer: coarse) and (max-width: 932px)';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {{matchMedia: function(string): {matches: boolean}}} win the window the page is laid out in
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
* @throws {TypeError} a window without matchMedia
|
|
35
|
+
*/
|
|
36
|
+
export function isMobileDevice(win) {
|
|
37
|
+
if (!win || typeof win.matchMedia !== 'function') {
|
|
38
|
+
throw new TypeError('[device] isMobileDevice(win) needs the window the page is laid out in (a matchMedia ' +
|
|
39
|
+
'function); got ' + (win === null ? 'null' : typeof win) + '. (FAIL-LOUD.)');
|
|
40
|
+
}
|
|
41
|
+
return win.matchMedia(MOBILE_DEVICE_QUERY).matches === true;
|
|
42
|
+
}
|
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
* parseManifest, serializeManifest, manifestFor manifest.js (THE owner of frqtl.json)
|
|
14
14
|
* RECORD_FILES, RECORD_KEYS, CHANGE_KEYS, OPS, BY,
|
|
15
15
|
* CLIP_SECONDS, CLIP_NAME, clipName, jsonText,
|
|
16
|
-
* recordFolderFaults, applyRows
|
|
17
|
-
* of its change rows' meaning:
|
|
16
|
+
* rowFaults, recordFolderFaults, applyRows record-format.js (THE owner of the Record folder's shape and
|
|
17
|
+
* of its change rows' shape and meaning: rowFaults checks one row,
|
|
18
|
+
* applyRows applies them to a block)
|
|
19
|
+
*
|
|
20
|
+
* The entry stands alone: it loads the parts roster (every published kind registers on import, objects/kinds.js),
|
|
21
|
+
* so applyRows can spell a 6-number rect by its kind's descriptor with nothing else imported. The roster is imported
|
|
22
|
+
* HERE, by the package door, and never by record-format.js: a scene page reaches record-format statically
|
|
23
|
+
* (RecipeContext → Record.js) and loads the roster only in its boot's modules phase, where a failed part is reported.
|
|
18
24
|
*
|
|
19
25
|
* PROJECT_EXPORTS is this entry's export list; a conformance test holds the module's exports equal to
|
|
20
26
|
* it, and the package gate holds it equal across @descix/frqtl-web and @descix/frqtl-node.
|
|
@@ -25,11 +31,14 @@ export const PROJECT_EXPORTS = Object.freeze(['FRQTL_WEB_SPECIFIERS', 'libPathOf
|
|
|
25
31
|
'linkProject', 'sketchProject', 'SKETCH_ENTRY', 'projectIdentity',
|
|
26
32
|
'MANIFEST_FILE', 'MANIFEST_KEYS', 'normalizeManifest', 'parseManifest', 'serializeManifest', 'manifestFor',
|
|
27
33
|
'RECORD_FILES', 'RECORD_KEYS', 'CHANGE_KEYS', 'OPS', 'BY', 'CLIP_SECONDS', 'CLIP_NAME', 'clipName', 'jsonText',
|
|
28
|
-
'recordFolderFaults', 'applyRows']);
|
|
34
|
+
'rowFaults', 'recordFolderFaults', 'applyRows']);
|
|
35
|
+
|
|
36
|
+
// The parts roster (see above): a side-effect import, so ./project stands alone.
|
|
37
|
+
import '../../frqtl/parts/index.js';
|
|
29
38
|
|
|
30
39
|
export { FRQTL_WEB_SPECIFIERS, libPathOf, importMapFor, pageImportMapFor, FRQTL_WEB_HOST_SPECIFIERS } from './specifiers.js';
|
|
31
40
|
export { linkProject, sketchProject, SKETCH_ENTRY } from './link.js';
|
|
32
41
|
export { projectIdentity } from './identity.js';
|
|
33
42
|
export { MANIFEST_FILE, MANIFEST_KEYS, normalizeManifest, parseManifest, serializeManifest, manifestFor } from './manifest.js';
|
|
34
43
|
export { RECORD_FILES, RECORD_KEYS, CHANGE_KEYS, OPS, BY, CLIP_SECONDS, CLIP_NAME, clipName, jsonText,
|
|
35
|
-
recordFolderFaults, applyRows } from './record-format.js';
|
|
44
|
+
rowFaults, recordFolderFaults, applyRows } from './record-format.js';
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* spellRect(props, { kind, entryKeys, baseline }) — a 6-tuple of a field whose descriptor derives reads from it
|
|
11
11
|
* (ObjectSchema.derivedReads: Wall `z`, `depth`) is written as the 4-tuple plus each derived key whose value
|
|
12
12
|
* changed, when the entry spells any of those keys as plain properties; otherwise the 6-tuple stands. A
|
|
13
|
-
* 4-tuple equal to the baseline's is not written (no literalizing an untouched expression).
|
|
13
|
+
* 4-tuple equal to the baseline's is not written (no literalizing an untouched expression). THROWS, naming the
|
|
14
|
+
* remedy, when `kind` is not registered in this realm (no imported module defines it).
|
|
14
15
|
* spellCenter(props, { entryKeys, centerLength }) — a `center` [x, y, 0] is written as two numbers when the entry
|
|
15
16
|
* spells two; otherwise the [x, y, z] stands.
|
|
16
17
|
* captureCenter(v) — the inverse: a spelled `center` in its capture form. [x, y] → [x, y, 0] (an omitted z is 0,
|
|
@@ -31,6 +32,12 @@ function _hasSix(props) {
|
|
|
31
32
|
/** @param {object} props @param {{ kind: string, entryKeys: function(): string[], baseline: object }} o */
|
|
32
33
|
export function spellRect(props, o) {
|
|
33
34
|
if (!_hasSix(props)) { return props; }
|
|
35
|
+
if (!ObjectSchema.get(o.kind)) {
|
|
36
|
+
throw new Error('[parts-spelling] the kind "' + o.kind + '" is not registered in this realm: a 6-number rect is spelled by ' +
|
|
37
|
+
'its descriptor, and a kind registers when the module that defines it is imported (the published parts register ' +
|
|
38
|
+
'with ./project; a project\'s own part, parts/' + o.kind + '.js, when its module is imported) — import the module ' +
|
|
39
|
+
'that defines it. (FAIL-LOUD.)');
|
|
40
|
+
}
|
|
34
41
|
const derived = ObjectSchema.derivedReads(o.kind);
|
|
35
42
|
if (!derived.length) { return props; }
|
|
36
43
|
const spelled = o.entryKeys();
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* CLIP_SECONDS { min, max } — the length of one clip, in seconds of playback
|
|
23
23
|
* CLIP_NAME the clip file pattern; clipName(n) → 'clip-NN.mp4' (n from 1)
|
|
24
24
|
* jsonText(v) JSON.stringify(v, null, 2) + '\n' — every JSON file of the folder
|
|
25
|
+
* rowFaults(row, at?) → string[] [] for a valid change row, every fault by name otherwise — THE one shape check of
|
|
26
|
+
* a row (its keys, tick, name, by, op in OPS, from/to per op); recordFolderFaults, applyRows and
|
|
27
|
+
* serializeRecordFolder ask it, so a host checking a row before it writes or applies one asks it too
|
|
25
28
|
* recordFolderFaults(files) → string[] [] for a complete §6 folder, every fault by name otherwise
|
|
26
29
|
* serializeRecordFolder(parts) → { files, record, clips } | { empty: true } THE writer (Record.js calls it;
|
|
27
30
|
* it runs recordFolderFaults on its own output and THROWS on any fault)
|
|
@@ -128,8 +131,17 @@ function _placement(v) {
|
|
|
128
131
|
|
|
129
132
|
const MOVE_SHAPE = 'move carries from/to as ONE key, {center: [x, y, z]} or {rect: [4 numbers flat | 6 with a z-extent]}, the same key and length in both';
|
|
130
133
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
134
|
+
/**
|
|
135
|
+
* rowFaults(row, at?) → string[] — THE shape check of one change row: [] when valid, otherwise every fault by name.
|
|
136
|
+
* Checks the keys (exactly CHANGE_KEYS), `tick` (a whole tick ≥ 0), `name` (the declared name), `by` (one of BY),
|
|
137
|
+
* `op` (one of OPS) and `from`/`to` (strict JSON, in that op's shape; a move that moves nothing is a fault).
|
|
138
|
+
* A row alone carries no folder facts: tickRange, tick order and one shape per name's moves are recordFolderFaults'.
|
|
139
|
+
* @param {*} c the row
|
|
140
|
+
* @param {string} [at='row'] the row's label at the head of each fault (e.g. 'changes[3]')
|
|
141
|
+
* @returns {string[]}
|
|
142
|
+
*/
|
|
143
|
+
export function rowFaults(c, at) {
|
|
144
|
+
if (at === undefined) { at = 'row'; }
|
|
133
145
|
const bad = [];
|
|
134
146
|
if (!_isPlain(c)) { return [at + ' is not a change {' + CHANGE_KEYS.join(', ') + '}']; }
|
|
135
147
|
const keys = Object.keys(c);
|
|
@@ -228,7 +240,7 @@ export function recordFolderFaults(files) {
|
|
|
228
240
|
let last = -Infinity;
|
|
229
241
|
rec.changes.forEach(function (c, i) {
|
|
230
242
|
const at = RECORD_FILES.record + ' changes[' + i + ']';
|
|
231
|
-
|
|
243
|
+
rowFaults(c, at).forEach(function (f) { bad.push(f); });
|
|
232
244
|
if (!_isPlain(c)) { return; }
|
|
233
245
|
if (Number.isInteger(c.tick)) {
|
|
234
246
|
if (range && (c.tick < range[0] || c.tick > range[1])) { bad.push(at + ' at tick ' + c.tick + ' lies outside tickRange ' + JSON.stringify(range)); }
|
|
@@ -332,7 +344,7 @@ export function serializeRecordFolder(p) {
|
|
|
332
344
|
// Check the rows as HELD, before any text: JSON.stringify writes NaN/Infinity as null and drops an undefined key,
|
|
333
345
|
// so a check of the written text would read a different value from the one the Record held.
|
|
334
346
|
const held = [];
|
|
335
|
-
record.changes.forEach(function (c, i) { held.push.apply(held,
|
|
347
|
+
record.changes.forEach(function (c, i) { held.push.apply(held, rowFaults(c, 'changes[' + i + ']')); });
|
|
336
348
|
if (held.length) { throw new Error('[record-format] a change row is not strict JSON: ' + held.join('; ') + ' (FAIL-LOUD.)'); }
|
|
337
349
|
const files = {};
|
|
338
350
|
files[RECORD_FILES.record] = jsonText(record);
|
|
@@ -369,12 +381,16 @@ function _centerLength(props) {
|
|
|
369
381
|
* fork entry `name`'s kind becomes `to.kind` (its props are unchanged)
|
|
370
382
|
* The spelling is the writer's own rule (parts-spelling.js spellRect / spellCenter: a 6-number rect beside spelled
|
|
371
383
|
* `z`/`depth` keys, a two-number center), read off the block, so a block a hand changed and the rows that change
|
|
372
|
-
* wrote agree: applyRows(parts.before, record.changes) is parts.after. A rect row
|
|
373
|
-
*
|
|
384
|
+
* wrote agree: applyRows(parts.before, record.changes) is parts.after. A 6-number rect row asks its kind's descriptor
|
|
385
|
+
* (ObjectSchema.derivedReads): the ./project entry (index.js) imports the parts roster, so every published kind is
|
|
386
|
+
* registered wherever ./project is loaded, and a scene page has registered them by its boot's modules phase; a
|
|
387
|
+
* project's own kind (a fork's parts/<Kind>.js) is registered when its module is imported. This module itself imports
|
|
388
|
+
* no part: the scene page reaches it statically (RecipeContext → Record.js), and a static barrel import here would load
|
|
389
|
+
* every part with the page module, ahead of the boot record (a failed part would then leave no boot banner).
|
|
374
390
|
* THROWS, naming the row index and the offending value: a block that is not { name: [kind name, {props}] }; rows
|
|
375
|
-
* that are not an array; a row
|
|
376
|
-
*
|
|
377
|
-
* `from` is not the entry's kind.
|
|
391
|
+
* that are not an array; a row rowFaults names (an op that is not one of OPS, its keys, its from/to shape — the
|
|
392
|
+
* faults in the error); a 6-number rect row on a kind no imported module registered; a name the block does not
|
|
393
|
+
* hold (move, setDeclared, remove, fork) or already holds (add); a fork whose `from` is not the entry's kind.
|
|
378
394
|
* @param {Object<string, [string, object]>} partsBlock
|
|
379
395
|
* @param {object[]} rows
|
|
380
396
|
* @returns {Object<string, [string, object]>}
|
|
@@ -386,7 +402,7 @@ export function applyRows(partsBlock, rows) {
|
|
|
386
402
|
JSON.stringify(rows) + '. (FAIL-LOUD.)');
|
|
387
403
|
}
|
|
388
404
|
rows.forEach(function (row, i) {
|
|
389
|
-
const faults =
|
|
405
|
+
const faults = rowFaults(row, 'rows[' + i + ']');
|
|
390
406
|
if (faults.length) { _applyFail(i, 'is not a change row: ' + faults.join('; ')); }
|
|
391
407
|
const name = row.name;
|
|
392
408
|
const has = Object.prototype.hasOwnProperty.call(block, name);
|
|
@@ -411,8 +427,10 @@ export function applyRows(partsBlock, rows) {
|
|
|
411
427
|
// move / setDeclared: the row's capture-form values, spelled in the entry's own form.
|
|
412
428
|
const props = JSON.parse(JSON.stringify(row.to));
|
|
413
429
|
const own = entry[1];
|
|
414
|
-
|
|
415
|
-
|
|
430
|
+
try {
|
|
431
|
+
spellRect(props, { kind: entry[0], baseline: row.from, entryKeys: function () { return Object.keys(own); } });
|
|
432
|
+
spellCenter(props, { entryKeys: function () { return Object.keys(own); }, centerLength: function () { return _centerLength(own); } });
|
|
433
|
+
} catch (e) { throw new Error('[record-format] applyRows: rows[' + i + '] (' + row.op + ') of "' + name + '": ' + ((e && e.message) || String(e))); }
|
|
416
434
|
block[name] = [entry[0], Object.assign({}, own, props)];
|
|
417
435
|
});
|
|
418
436
|
return block;
|
|
@@ -162,9 +162,18 @@ assertShowcaseIntro(SHOWCASE_INTRO);
|
|
|
162
162
|
// (the egpt-frqtl front page's #proof tiles: each tile's <b> and its sentence), verbatim;
|
|
163
163
|
// `what`, `why`, `how`, `controls` and each `still.alt` are the authored captions.
|
|
164
164
|
const ROSTER = [
|
|
165
|
+
{
|
|
166
|
+
kind: 'rung', id: "cl1d-mirror-route", mode: "3d",
|
|
167
|
+
still: { tick: 3600, alt: "Ten brightly coloured mirror blocks in a ring, seen face-on, one cyan block on the right standing inside the ring out of its place; a thick, glowing band of gold, red, pink and green light swirls around a dark centre holding faint purple and green eddies, and green and pink rays reach out between the mirrors into the dark." },
|
|
168
|
+
title: "Infinite mirror",
|
|
169
|
+
tile: "Ten mirrors in a ring. Light walks the loop. No Fresnel equations.",
|
|
170
|
+
what: "Ten mirrors in a ring. Light goes round and round the loop, reflected and diffused from mirror to mirror, each mirror colouring the light it sends on.",
|
|
171
|
+
why: "Conventional renderers cut every light path off after a set number of bounces and treat shadows, reflections and indirect light as separate effects. Solving the light directly in a 5 m room would take Frontier about 3 billion years.",
|
|
172
|
+
how: "One emitter and ten walls set as mirrors. Light leaves in every direction; shadowing, reflection and obstruction are all live, with no limit on the objects in its path, and the diffusion is real. No Fresnel equations.",
|
|
173
|
+
},
|
|
165
174
|
{
|
|
166
175
|
kind: 'rung', id: "dse-tunnel-rebuild", mode: "3d",
|
|
167
|
-
still: { tick: 7200, alt: "Three red blocks with blue slit walls between them
|
|
176
|
+
still: { tick: 7200, alt: "Three red blocks with blue slit walls between them, seen at a tilt from behind the source; a fan of yellow rays spreads from a point at lower left into the slits, and beyond them, at upper right, a heat-map plate of blue and green squares shows light and dark bands with a red hot spot at its centre." },
|
|
168
177
|
title: "Double slit",
|
|
169
178
|
tile: "Two slits, one particle at a time. Feynman's “only mystery,” with no amplitudes to add.",
|
|
170
179
|
what: "Particles go through two slits one at a time, and bright and dark bands build up on the detector plate below them, live in your browser tab: the pattern Feynman called quantum mechanics' \"only mystery\". EGPT conceptually holds computer science and physics are linked by P=NP and the principle of least action - that is, everything in reality is efficiently computable. The Lean RET, the derivation of EntropyReal from an IIDParticleSource, and the P=NP capstones are modern formalizations of proofs and ideas advanced by Gian-Carlo Rota circa the late 1970s. The EGPT Lean proofs transparently show the most rigorously advanced proofs ever offered for bridging not only quantum and classical mechanics but mathematics and all the other sciences. Watch it run, read its code, and judge for yourself whether this is true P=NP quantum computing.",
|
|
@@ -180,15 +189,6 @@ const ROSTER = [
|
|
|
180
189
|
why: "Games paint glow and god rays onto the finished picture as a separate screen effect, aimed from where the sun is on the screen rather than from any light in the scene.",
|
|
181
190
|
how: "One sphere emitter, shining in every direction. The god rays are rendered from the density of the particles themselves. No Standard Model, no equations, and the engine does not know whether you are looking at a star, a candle flame or an atom.",
|
|
182
191
|
},
|
|
183
|
-
{
|
|
184
|
-
kind: 'rung', id: "cl1d-mirror-route", mode: "3d",
|
|
185
|
-
still: { tick: 1200, alt: "Ten brightly coloured mirrors in a ring, seen from above; a dense band of gold, orange and green dots circles a dark, empty centre inside it, and faint gold and green rays fan out from that light between the mirrors into the dark space around." },
|
|
186
|
-
title: "Infinite mirror",
|
|
187
|
-
tile: "Ten mirrors in a ring. Light walks the loop. No Fresnel equations.",
|
|
188
|
-
what: "Ten mirrors in a ring. Light goes round and round the loop, reflected and diffused from mirror to mirror, each mirror colouring the light it sends on.",
|
|
189
|
-
why: "Conventional renderers cut every light path off after a set number of bounces and treat shadows, reflections and indirect light as separate effects. Solving the light directly in a 5 m room would take Frontier about 3 billion years.",
|
|
190
|
-
how: "One emitter and ten walls set as mirrors. Light leaves in every direction; shadowing, reflection and obstruction are all live, with no limit on the objects in its path, and the diffusion is real. No Fresnel equations.",
|
|
191
|
-
},
|
|
192
192
|
{
|
|
193
193
|
kind: 'rung', id: "r5-fountain-pool",
|
|
194
194
|
still: { tick: 300, alt: "A dark teal surface scattered with white glints like light on water, with a faint ring around a dark dot at its centre." },
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* its error copy.
|
|
9
9
|
*
|
|
10
10
|
* STUDIO_LANDING_ATTR the <html> attribute carrying the outcome id
|
|
11
|
-
* STUDIO_LANDING_OUTCOMES the closed table: one row per branch of STUDIO's link resolution, `{ id, means }
|
|
11
|
+
* STUDIO_LANDING_OUTCOMES the closed table: one row per branch of STUDIO's link resolution, `{ id, runs, means }`;
|
|
12
|
+
* `runs` says whether the scene that landing opens starts on load (through STUDIO's Run
|
|
13
|
+
* control's own path) — true only on `ok`, a deep link that named a scene; every other
|
|
14
|
+
* landing opens its scene in Design, waiting for Run
|
|
12
15
|
* recordStudioLanding(root, id) writes `id` on `root` (an Element, or null to only validate); an id outside the
|
|
13
16
|
* table THROWS — that is a defect in the writer, never a landing
|
|
14
17
|
*
|
|
@@ -29,23 +32,23 @@ export const STUDIO_LANDING_ATTR = 'data-frqtl-studio-landing';
|
|
|
29
32
|
|
|
30
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). */
|
|
31
34
|
export const STUDIO_LANDING_OUTCOMES = Object.freeze([
|
|
32
|
-
{ id: 'refused-key',
|
|
35
|
+
{ id: 'refused-key', runs: false,
|
|
33
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 rung opens' },
|
|
34
|
-
{ id: 'conflict',
|
|
37
|
+
{ id: 'conflict', runs: false,
|
|
35
38
|
means: 'the link names both a walkthrough and a saved project: nothing opens' },
|
|
36
|
-
{ id: 'mode-without-walkthrough',
|
|
39
|
+
{ id: 'mode-without-walkthrough', runs: false,
|
|
37
40
|
means: 'the link names a mode and no walkthrough: nothing opens' },
|
|
38
|
-
{ id: 'unknown-walkthrough',
|
|
41
|
+
{ id: 'unknown-walkthrough', runs: false,
|
|
39
42
|
means: 'the linked walkthrough is not in the walkthrough registry (walkthroughById): nothing opens' },
|
|
40
|
-
{ id: 'unknown-mode',
|
|
43
|
+
{ id: 'unknown-mode', runs: false,
|
|
41
44
|
means: 'the linked mode is not one the linked rung declares (walkthroughModes): nothing opens' },
|
|
42
|
-
{ id: 'project-unreadable',
|
|
45
|
+
{ id: 'project-unreadable', runs: false,
|
|
43
46
|
means: 'the linked saved project is present but its folder is refused by the load: nothing opens' },
|
|
44
|
-
{ id: 'project-not-found',
|
|
47
|
+
{ id: 'project-not-found', runs: false,
|
|
45
48
|
means: 'no saved project of the linked name in this browser: nothing opens' },
|
|
46
|
-
{ id: 'ok',
|
|
47
|
-
means: 'the linked walkthrough (in the linked mode, else its default) or saved project resolved and was handed to STUDIO\'s selection path' },
|
|
48
|
-
{ id: 'first-contact',
|
|
49
|
+
{ id: 'ok', runs: true,
|
|
50
|
+
means: 'the linked walkthrough (in the linked mode, else its default) or saved project resolved and was handed to STUDIO\'s selection path; the scene starts on load' },
|
|
51
|
+
{ id: 'first-contact', runs: false,
|
|
49
52
|
means: 'no STUDIO deep link: the walkthrough registry\'s opening rung opens' },
|
|
50
53
|
].map(Object.freeze));
|
|
51
54
|
|