@descix/frqtl-node 0.6.1-p3.2 → 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 +1 -1
- package/dist/dist-manifest.json +21 -21
- 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/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 +1 -1
- 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 +6 -2
- package/dist/shim/registry/exhibits.js +10 -10
- 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
|
@@ -258,6 +258,13 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
258
258
|
description: "What the god-rays are computed from: 'leaf' = the fundamental-layer frames " +
|
|
259
259
|
"(LOs only occlude), 'all' = the whole lit scene (every frame of every layer and every LO)."
|
|
260
260
|
}),
|
|
261
|
+
normalizeFieldDescriptor({
|
|
262
|
+
name: 'godrayMode', type: 'enum', default: GodraysLayer.GODRAY_MODE_DEFAULT,
|
|
263
|
+
constraint: { values: GodraysLayer.GODRAY_MODES.reduce(function (m, v) { m[v] = v; return m; }, {}) },
|
|
264
|
+
description: "How the god-rays are computed: 'streak' = each source frame trails a streak behind " +
|
|
265
|
+
"its direction of travel, 'field' = each pixel marches along the local direction of travel, " +
|
|
266
|
+
"'radial' = each pixel marches toward one light point (the source frames' centroid)."
|
|
267
|
+
}),
|
|
261
268
|
normalizeFieldDescriptor({
|
|
262
269
|
name: 'glowIntensity', type: 'number', unit: 'intensity', default: 0,
|
|
263
270
|
constraint: { min: 0, max: 3, integer: false },
|
|
@@ -614,6 +621,9 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
614
621
|
// ── R4-B god-rays layer (GodraysLayer.makeGodrayPasses; created lazily on first activation) ──
|
|
615
622
|
var _godrays = null;
|
|
616
623
|
var _cubeSourceCount = 0; // cube instances [0, n) are god-rays source frames
|
|
624
|
+
// The particle objects LAID this tick but hidden from the lit picture (Particles OFF while
|
|
625
|
+
// god rays consume the particle layer): the source pass shows them, the lit scene never does.
|
|
626
|
+
var _particlesLaidHidden = [];
|
|
617
627
|
|
|
618
628
|
var _initialized = false;
|
|
619
629
|
var _lastTickData = null;
|
|
@@ -1260,14 +1270,14 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1260
1270
|
// Position one clone of the loaded mesh per frame (reusing a pool). The loaded scene is
|
|
1261
1271
|
// unit-normalized; each clone is scaled to the frame's voxel size (w) and placed at the
|
|
1262
1272
|
// frame's (x,y,z) — the SAME physics coords the cube/points paths use. Extra clones hide.
|
|
1263
|
-
function _layoutMeshClones(entry, frameData,
|
|
1273
|
+
function _layoutMeshClones(entry, frameData, lay, scaleMul, assetId) {
|
|
1264
1274
|
if (!_meshGroup) { _meshGroup = new THREE.Group(); _meshGroup.frustumCulled = false; _scene.add(_meshGroup); }
|
|
1265
1275
|
// Per-ASSET clone sub-pool (spec §2): one pool per asset id so concurrent assets never
|
|
1266
1276
|
// fight over slots. Callers: the scene-wide useMesh branch (assetId = resolvedMesh) and
|
|
1267
1277
|
// the per-frame mesh siphon in the Points path (one call per stamped asset).
|
|
1268
1278
|
var pool = _meshClonesByAsset[assetId] || (_meshClonesByAsset[assetId] = []);
|
|
1269
1279
|
var mul = (typeof scaleMul === 'number' && scaleMul > 0) ? scaleMul : 1;
|
|
1270
|
-
var n = (
|
|
1280
|
+
var n = (lay && frameData) ? frameData.length : 0;
|
|
1271
1281
|
for (var i = 0; i < n; i++) {
|
|
1272
1282
|
var f = frameData[i];
|
|
1273
1283
|
var clone = pool[i];
|
|
@@ -1491,10 +1501,19 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1491
1501
|
|
|
1492
1502
|
// The god-rays SOURCE image → _godrays.rtSource: same scene/camera/paint, ONLY the fundamental-layer
|
|
1493
1503
|
// frames drawn. LOs write depth but no colour (they shadow, never shine). All flags restored.
|
|
1494
|
-
function _renderGodraySource(tickData) {
|
|
1504
|
+
function _renderGodraySource(tickData, leaf) {
|
|
1495
1505
|
var gs = GodraysLayer, fund = gs.fundamentalLayerOf(tickData);
|
|
1496
1506
|
var hidden = [], cubeCount = null, colourOff = [];
|
|
1497
1507
|
function hide(o) { if (o && o.visible) { o.visible = false; hidden.push(o); } }
|
|
1508
|
+
// The rays read the PARTICLE LAYER whether or not the lit picture draws it.
|
|
1509
|
+
for (var p = 0; p < _particlesLaidHidden.length; p++) { _particlesLaidHidden[p].visible = true; }
|
|
1510
|
+
if (!leaf) {
|
|
1511
|
+
// 'all': the lit scene WITH its particles (only reached when Particles are OFF).
|
|
1512
|
+
_renderer.setRenderTarget(_godrays.rtSource);
|
|
1513
|
+
_renderer.render(_scene, _camera);
|
|
1514
|
+
for (var pa = 0; pa < _particlesLaidHidden.length; pa++) { _particlesLaidHidden[pa].visible = false; }
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1498
1517
|
for (var k in _dimLayers) {
|
|
1499
1518
|
if (_dimLayers.hasOwnProperty(k) && !gs.isGodraySource(_dimLayers[k].layer, fund)) { hide(_dimLayers[k].mesh); }
|
|
1500
1519
|
}
|
|
@@ -1522,19 +1541,36 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1522
1541
|
for (var h = 0; h < hidden.length; h++) { hidden[h].visible = true; }
|
|
1523
1542
|
if (cubeCount !== null) { _cubeMesh.count = cubeCount; }
|
|
1524
1543
|
for (var c = 0; c < colourOff.length; c++) { colourOff[c].colorWrite = true; }
|
|
1544
|
+
for (var q = 0; q < _particlesLaidHidden.length; q++) { _particlesLaidHidden[q].visible = false; }
|
|
1525
1545
|
}
|
|
1526
1546
|
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
1529
|
-
|
|
1547
|
+
// Are god rays on? THE one predicate: the final composite dispatch and the particle-layer
|
|
1548
|
+
// gate (rays consume the particle layer as their source) both ask it.
|
|
1549
|
+
function _wantGodrays(opts) { return opts.godrays > 0; }
|
|
1550
|
+
|
|
1551
|
+
// God-rays to screen: the rays read the SOURCE image (fundamental-layer frames, LOs
|
|
1552
|
+
// depth-only) and add over the full lit scene. Cost paid ONLY when on.
|
|
1553
|
+
// `godraySource` 'all' reads the lit scene itself (every frame, every LO); `godrayMode`
|
|
1554
|
+
// picks the pass (GodraysLayer owns both values and defaults).
|
|
1530
1555
|
function _renderGodrays(opts, tickData) {
|
|
1531
1556
|
var gl = GodraysLayer, leaf = gl.resolveGodraySource(opts.godraySource) === 'leaf';
|
|
1557
|
+
var mode = gl.resolveGodrayMode(opts.godrayMode);
|
|
1532
1558
|
if (!_godrays) { _godrays = gl.makeGodrayPasses(THREE, QUAD_VERT, _physW(), _physH()); }
|
|
1533
1559
|
_renderer.setRenderTarget(_rtA); // Pass 1: lit scene → _rtA
|
|
1534
1560
|
_renderer.render(_scene, _camera);
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1561
|
+
// Pass 1b: source → rtSource. 'leaf' always; 'all' only when the lit scene lacks its
|
|
1562
|
+
// particles (Particles OFF) — otherwise the lit scene IS the 'all' source.
|
|
1563
|
+
var ownSource = leaf || _particlesLaidHidden.length > 0;
|
|
1564
|
+
if (ownSource) { _renderGodraySource(tickData, leaf); }
|
|
1565
|
+
var src = ownSource ? _godrays.rtSource : _rtA;
|
|
1566
|
+
if (mode === 'radial') {
|
|
1567
|
+
_godrays.march(_renderer, _quadCamera, _rtA, src, _rtB,
|
|
1568
|
+
_godrays.lightUV(tickData, _camera, opts.godraySource), opts.godrayDensity, opts.godrays);
|
|
1569
|
+
} else {
|
|
1570
|
+
var pass = (mode === 'streak') ? _godrays.streak : _godrays.field;
|
|
1571
|
+
pass(_renderer, _quadCamera, _rtA, src, _rtB,
|
|
1572
|
+
gl.godraySourceFrames(tickData, opts.godraySource), _camera, opts.godrayDensity, opts.godrays);
|
|
1573
|
+
}
|
|
1538
1574
|
}
|
|
1539
1575
|
|
|
1540
1576
|
// ── Aim feedback layer update ─────────────────────────────────────────────────
|
|
@@ -1729,7 +1765,10 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1729
1765
|
// built ONLY when a consumer (a queued filter) is active — never unconditionally (that was one sugar object hijacking the
|
|
1730
1766
|
// render loop). Each flag comes from the render OPTIONS bag (setRenderOption).
|
|
1731
1767
|
var opts = options || {};
|
|
1732
|
-
var drawParticles = opts.renderBaseCanvas !== false; // Particles layer
|
|
1768
|
+
var drawParticles = opts.renderBaseCanvas !== false; // Particles layer (the lit picture)
|
|
1769
|
+
// The particle layer is LAID when drawn OR consumed: god rays read it as their source,
|
|
1770
|
+
// so Particles OFF + god rays ON lays it out hidden and the rays still shine.
|
|
1771
|
+
var layParticles = drawParticles || _wantGodrays(opts);
|
|
1733
1772
|
// SNAPSHOT, not the reference: _renderOptsWithFeedback() returns
|
|
1734
1773
|
// this._renderOptions ITSELF when there is no aim-feedback bag, so retaining
|
|
1735
1774
|
// `opts` would let a setRenderOption between ticks silently change what
|
|
@@ -1783,7 +1822,7 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1783
1822
|
var _me = _meshCache[resolvedMesh];
|
|
1784
1823
|
var _sceneLaid = 0;
|
|
1785
1824
|
if (_me && _me.status === 'ready') {
|
|
1786
|
-
_sceneLaid = _layoutMeshClones(_me, tickData.frameData,
|
|
1825
|
+
_sceneLaid = _layoutMeshClones(_me, tickData.frameData, layParticles, opts.meshScale, resolvedMesh);
|
|
1787
1826
|
}
|
|
1788
1827
|
_hideMeshClonesExcept([resolvedMesh]); // drop any stale per-frame/other-asset clones
|
|
1789
1828
|
if (_meshGroup) { _meshGroup.visible = _sceneLaid > 0; } // loading/empty → hidden (error already overlaid)
|
|
@@ -1795,7 +1834,7 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1795
1834
|
// bright; back/bottom are dark — so the cube reads as a solid 3D object.
|
|
1796
1835
|
_hideAllDimLayers();
|
|
1797
1836
|
|
|
1798
|
-
if (
|
|
1837
|
+
if (layParticles && tickData && tickData.frameData) {
|
|
1799
1838
|
var fdC = tickData.frameData;
|
|
1800
1839
|
var totalC = fdC.length;
|
|
1801
1840
|
var defaultColorSchemeC = opts.colorScheme || 'warmcool';
|
|
@@ -1847,7 +1886,7 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1847
1886
|
_cubeMesh.count = cubeCount;
|
|
1848
1887
|
_cubeMesh.instanceMatrix.needsUpdate = true;
|
|
1849
1888
|
if (_cubeMesh.instanceColor) { _cubeMesh.instanceColor.needsUpdate = true; }
|
|
1850
|
-
_cubeMesh.visible =
|
|
1889
|
+
_cubeMesh.visible = cubeCount > 0;
|
|
1851
1890
|
} else {
|
|
1852
1891
|
if (_cubeMesh) { _cubeMesh.visible = false; }
|
|
1853
1892
|
}
|
|
@@ -1858,7 +1897,7 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
1858
1897
|
if (_cubeMesh) { _cubeMesh.visible = false; }
|
|
1859
1898
|
_hideAllDimLayers();
|
|
1860
1899
|
|
|
1861
|
-
if (
|
|
1900
|
+
if (layParticles && tickData && tickData.frameData) {
|
|
1862
1901
|
var fd = tickData.frameData;
|
|
1863
1902
|
var total = fd.length;
|
|
1864
1903
|
|
|
@@ -2039,7 +2078,7 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
2039
2078
|
}
|
|
2040
2079
|
|
|
2041
2080
|
entry.centroid.x = cx / n; entry.centroid.y = cy / n; entry.centroid.z = cz / n;
|
|
2042
|
-
entry.mesh.visible =
|
|
2081
|
+
entry.mesh.visible = true;
|
|
2043
2082
|
}
|
|
2044
2083
|
|
|
2045
2084
|
// ── Camera-distance sort (painter's algorithm) — farthest
|
|
@@ -2070,7 +2109,7 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
2070
2109
|
_ensureMeshLoaded(_aid);
|
|
2071
2110
|
var _pfMe = _meshCache[_aid];
|
|
2072
2111
|
if (_pfMe && _pfMe.status === 'ready') {
|
|
2073
|
-
_pfMeshLaid += _layoutMeshClones(_pfMe, meshByAsset[_aid],
|
|
2112
|
+
_pfMeshLaid += _layoutMeshClones(_pfMe, meshByAsset[_aid], layParticles, opts.meshScale, _aid);
|
|
2074
2113
|
}
|
|
2075
2114
|
}
|
|
2076
2115
|
_hideMeshClonesExcept(_pfMeshAssets);
|
|
@@ -2078,6 +2117,16 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
2078
2117
|
}
|
|
2079
2118
|
}
|
|
2080
2119
|
|
|
2120
|
+
// ── Particles OFF: what was laid (for the god-rays source) leaves the lit picture ──
|
|
2121
|
+
_particlesLaidHidden = [];
|
|
2122
|
+
if (!drawParticles) {
|
|
2123
|
+
var laid = [_cubeMesh, _meshGroup];
|
|
2124
|
+
for (var lk in _dimLayers) { if (_dimLayers.hasOwnProperty(lk)) { laid.push(_dimLayers[lk].mesh); } }
|
|
2125
|
+
for (var li = 0; li < laid.length; li++) {
|
|
2126
|
+
if (laid[li] && laid[li].visible) { laid[li].visible = false; _particlesLaidHidden.push(laid[li]); }
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2081
2130
|
// ── Field layer — build the heightfield ONLY when consumed ──
|
|
2082
2131
|
if (fieldConsumed) { _updateFieldTex(tickData); }
|
|
2083
2132
|
|
|
@@ -2098,7 +2147,7 @@ import * as ThreeCameraGeometry from './three-camera-geometry.js';
|
|
|
2098
2147
|
opts = opts || {};
|
|
2099
2148
|
// godrays is a final-composite layer (a declared render option). It owns the final pass when on; it and the 2D water-fx
|
|
2100
2149
|
// filter are alternative final composites (concurrent use is a phase-2 item).
|
|
2101
|
-
var wantGodrays = opts
|
|
2150
|
+
var wantGodrays = _wantGodrays(opts);
|
|
2102
2151
|
var time = _filters.length > 0
|
|
2103
2152
|
? RendererProviderContract.filterTime(tickData) : null;
|
|
2104
2153
|
_lastFilterTime = time; // capture provenance (§1a.4)
|
|
@@ -2010,6 +2010,19 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
|
|
|
2010
2010
|
return [x, y];
|
|
2011
2011
|
};
|
|
2012
2012
|
|
|
2013
|
+
/**
|
|
2014
|
+
* isPointable(d) → whether declared object `d` can be under a pointer on the canvas: at least one of
|
|
2015
|
+
* its members is not hidden by its own `visible:false`. The ONE rule both canvas picks read — the 3D
|
|
2016
|
+
* pick below (per member) and the host's 2D box hit-test (per object).
|
|
2017
|
+
*/
|
|
2018
|
+
FrqtlApp.prototype.isPointable = function (d) {
|
|
2019
|
+
var ms = this.membersOf(d);
|
|
2020
|
+
for (var i = 0; i < ms.length; i++) { if (_memberPointable(ms[i])) { return true; } }
|
|
2021
|
+
return false;
|
|
2022
|
+
};
|
|
2023
|
+
/** A member is under the pointer unless its own `visible:false` hides it. */
|
|
2024
|
+
function _memberPointable(lo) { return lo.visible !== false; }
|
|
2025
|
+
|
|
2013
2026
|
/** Screen radius (in the caller's screen units) inside which a click picks an object. */
|
|
2014
2027
|
FrqtlApp.PICK_RADIUS_PX = 32;
|
|
2015
2028
|
/** A center-bearing (drivable) object's pick distance is divided by this, so in a dense scene
|
|
@@ -2024,7 +2037,9 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
|
|
|
2024
2037
|
* anisotropic on screen and different per plane, which let a wall behind the emitter win inside
|
|
2025
2038
|
* the emitter's own footprint. A drivable object's distance is divided by PICK_DRIVE_PRIORITY.
|
|
2026
2039
|
* Objects-layer; consumed downward by AuthorView. Null (fail-soft) with no screenToWorld
|
|
2027
|
-
* capability — the host falls back to the Layers panel.
|
|
2040
|
+
* capability — the host falls back to the Layers panel. A member its own `visible:false` hides
|
|
2041
|
+
* is never under the pointer (isPointable): an invisible counter spanning the scene would
|
|
2042
|
+
* otherwise answer every press. The Layers panel still selects it.
|
|
2028
2043
|
*/
|
|
2029
2044
|
FrqtlApp.prototype.pickObjectAtScreen = function (cssX, cssY, cssW, cssH) {
|
|
2030
2045
|
var r = this.renderer;
|
|
@@ -2035,6 +2050,7 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
|
|
|
2035
2050
|
var lo = los[i];
|
|
2036
2051
|
var owner = this.declaredOf(lo);
|
|
2037
2052
|
if (!owner) { continue; } // a primitive-surface object is never picked
|
|
2053
|
+
if (!_memberPointable(lo)) { continue; } // hidden: not under the pointer (isPointable)
|
|
2038
2054
|
var c = this._objectCentroid(lo);
|
|
2039
2055
|
if (!c) { continue; }
|
|
2040
2056
|
var sp = this.screenPointOf(c, cssW, cssH, [cssX, cssY]);
|
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
* - setDeclared refuses while a hand is unwritten (await exp.toSketch() first); its `kept` lists what it kept.
|
|
54
54
|
* - restart() of a linked sketch re-links the CURRENT text (the retained replay) and returns a Promise.
|
|
55
55
|
* exp.runner / .universe the runner the co-init call built
|
|
56
|
+
* exp.sceneExports the named exports of the scene module the last build linked (its module
|
|
57
|
+
* namespace — the module that built this world, never a second evaluation);
|
|
58
|
+
* reading it THROWS when the scene was built from a function, which has none
|
|
56
59
|
* exp.is3D world.renderer === 'three'
|
|
57
60
|
*
|
|
58
61
|
* FAIL-LOUD: every member that needs the world THROWS before the call; a second call in one
|
|
@@ -126,6 +129,7 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
126
129
|
this._buildFn = null;
|
|
127
130
|
this._project = null; // THE held project text: { files: {path: text}, entry, mint, bare? } (build(linked))
|
|
128
131
|
this._urls = null; // the minted URL of each project file, from the last link
|
|
132
|
+
this._module = null; // the entry module's namespace, from the last link (exp.sceneExports)
|
|
129
133
|
this._parts = new Map(); // declared object → { key, baseline } — the objects in `parts`
|
|
130
134
|
this._code = new Map(); // declared object → baseline — the code declarations at build end
|
|
131
135
|
this._declaredOnce = false;
|
|
@@ -148,6 +152,18 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
148
152
|
Object.defineProperty(RecipeContext.prototype, 'is3D', {
|
|
149
153
|
get: function () { _requireWorld(this); return this._world.renderer === 'three'; }
|
|
150
154
|
});
|
|
155
|
+
/** READ-ONLY: the named exports of the scene module the last build linked (the module that built this world). */
|
|
156
|
+
Object.defineProperty(RecipeContext.prototype, 'sceneExports', {
|
|
157
|
+
get: function () {
|
|
158
|
+
_requireWorld(this);
|
|
159
|
+
if (!this._module) {
|
|
160
|
+
throw new Error('[RecipeContext] sceneExports: this world was built from a buildExperiment function, ' +
|
|
161
|
+
'not a linked scene module, so it has no module exports to read — build it from its text: ' +
|
|
162
|
+
'exp.build(await linkProject({ files, entry, mint })) (lib/shim/project/link.js). (FAIL-LOUD.)');
|
|
163
|
+
}
|
|
164
|
+
return this._module;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
151
167
|
|
|
152
168
|
// ── the co-init call ─────────────────────────────────────────────────────────────
|
|
153
169
|
|
|
@@ -893,6 +909,7 @@ import { Record, openRecordOf } from './record/Record.js';
|
|
|
893
909
|
this._buildFn = sketch;
|
|
894
910
|
this._project = linked ? linked.project : null;
|
|
895
911
|
this._urls = linked ? (linked.urls || null) : null;
|
|
912
|
+
this._module = linked ? linked.module : null;
|
|
896
913
|
if (this._runner) { _teardownSceneForRebuild(this); }
|
|
897
914
|
this._world = null;
|
|
898
915
|
this._parts = new Map();
|
|
@@ -53,18 +53,12 @@ import { linkProject } from '../../../shim/project/link.js';
|
|
|
53
53
|
import { serializeRecordFolder, CLIP_SECONDS, BY, CHANGE_KEYS } from '../../../shim/project/record-format.js';
|
|
54
54
|
import { kindNamed } from '../kinds.js';
|
|
55
55
|
import { _setOpenRecord, openRecordOf } from './open-record.js';
|
|
56
|
+
import { base64ToBytes } from '../../../shim/render/capture-envelope.js';
|
|
56
57
|
|
|
57
58
|
function _fail(msg) {
|
|
58
59
|
throw new Error('[Record] ' + msg + ' (FAIL-LOUD.)');
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function _bytesOfBase64(b64) {
|
|
62
|
-
var bin = atob(b64);
|
|
63
|
-
var out = new Uint8Array(bin.length);
|
|
64
|
-
for (var i = 0; i < bin.length; i++) { out[i] = bin.charCodeAt(i); }
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
62
|
/** linkProject(held).module.parts → { name: [registered kind name, props] } in key order. */
|
|
69
63
|
async function _partsOf(held) {
|
|
70
64
|
var linked = await linkProject(held);
|
|
@@ -223,7 +217,7 @@ Record.prototype._clipFor = function (req) {
|
|
|
223
217
|
settle();
|
|
224
218
|
if (!self._open) { return; }
|
|
225
219
|
var n = env.track.frames.length;
|
|
226
|
-
self._clips.push({ bytes:
|
|
220
|
+
self._clips.push({ bytes: base64ToBytes(env.encoded.bytes), firstTick: ticks[0], lastTick: ticks[n - 1] });
|
|
227
221
|
},
|
|
228
222
|
drop: function () { settle(); }
|
|
229
223
|
};
|
|
@@ -257,7 +251,8 @@ Record.prototype.film = function (opts) {
|
|
|
257
251
|
' equal whole-frame segments of at most ' + CLIP_SECONDS.max + ' s — choose seconds × fps divisible by ' + n + '.');
|
|
258
252
|
}
|
|
259
253
|
var f = total / n;
|
|
260
|
-
var
|
|
254
|
+
var b = FrqtlVision.recordBudget(f, every);
|
|
255
|
+
var req = { everyTicks: every, fps: fps, maxFrames: b.maxFrames, stopAt: b.stopAt };
|
|
261
256
|
if (opts.stage !== undefined) { req.stage = opts.stage; }
|
|
262
257
|
var vision = FrqtlVision.attach(this._runner);
|
|
263
258
|
var first = this._clips.length;
|
|
@@ -37,8 +37,10 @@
|
|
|
37
37
|
* getScene():Promise<object>, // the run-invariant SceneRecord + its content id
|
|
38
38
|
* getStill(stage?):Promise<object>, // stage omitted → the FULL composed stack
|
|
39
39
|
* getStream({stage?, everyTicks}):AsyncIterable<object>, // stills at a TICK cadence
|
|
40
|
-
* record({
|
|
40
|
+
* record({seconds?, fps?, signal?}) // THE DEFAULT: up to 60 s of what the viewer sees, wall-clock timed
|
|
41
41
|
* :Promise<object>, // an encoded video: CaptureEnvelope.videoEnvelope
|
|
42
|
+
* record({everyTicks, stopAt, maxFrames, fps, stage?, bitrate?, keyFrameInterval?, signal?})
|
|
43
|
+
* :Promise<object>, // the ADVANCED tick-sampling form (corpus rows, flights)
|
|
42
44
|
* stages:string[], // the FERRIED RenderStages.RENDER_STAGES list
|
|
43
45
|
* finalStage():string, // the ferried final-stage derivation
|
|
44
46
|
* toDataURL(still):string, // the BROWSER PNG encoder (data URL)
|
|
@@ -165,8 +167,12 @@
|
|
|
165
167
|
* var still = await vision.getStill(); // the whole composed stack
|
|
166
168
|
* var field = await vision.getStill('field'); // the render-free numeric channel
|
|
167
169
|
*
|
|
168
|
-
* // A video
|
|
169
|
-
* var
|
|
170
|
+
* // A video of what the viewer sees (the default form): up to 60 s at 30 fps; Stop = abort.
|
|
171
|
+
* var stop = new AbortController();
|
|
172
|
+
* var env = await vision.record({ seconds: 20, signal: stop.signal });
|
|
173
|
+
*
|
|
174
|
+
* // ADVANCED — tick sampling: 1 still per 2 ticks until tick 600, played back at 30 fps.
|
|
175
|
+
* var env2 = await vision.record({ everyTicks: 2, stopAt: { tick: 600 }, maxFrames: 400, fps: 30 });
|
|
170
176
|
*
|
|
171
177
|
* // Aligned multi-channel corpus row: capture BETWEEN ticks and assert equal tick stamps —
|
|
172
178
|
* // the tick equality IS the co-registration guard; `scene` ties them to one header.
|
|
@@ -462,33 +468,43 @@ function _getStream(runner, RS, opts) {
|
|
|
462
468
|
}
|
|
463
469
|
|
|
464
470
|
/** The recorder's declared options. @private */
|
|
465
|
-
var RECORD_OPTIONS = ['everyTicks', 'stopAt', 'maxFrames', 'fps', 'stage', 'bitrate', 'keyFrameInterval', 'signal'];
|
|
471
|
+
var RECORD_OPTIONS = ['seconds', 'everyTicks', 'stopAt', 'maxFrames', 'fps', 'stage', 'bitrate', 'keyFrameInterval', 'signal'];
|
|
472
|
+
/** The keys only the advanced TICK form takes; any of them selects it. @private */
|
|
473
|
+
var TICK_FORM_KEYS = ['everyTicks', 'stopAt', 'maxFrames'];
|
|
474
|
+
/**
|
|
475
|
+
* The default — HUMAN — form's defaults and cap: a recording of what the viewer sees, `seconds` long (at most
|
|
476
|
+
* RECORD_DEFAULTS.seconds), played back at `fps`.
|
|
477
|
+
*/
|
|
478
|
+
var RECORD_DEFAULTS = Object.freeze({ seconds: 60, fps: 30 });
|
|
479
|
+
var HUMAN_SHAPE = '{seconds?, fps?, signal?, stage?, bitrate?, keyFrameInterval?} (the default: seconds of what the viewer sees)';
|
|
480
|
+
var TICK_SHAPE = '{everyTicks, stopAt, maxFrames, fps, stage?, bitrate?, keyFrameInterval?, signal?} (the advanced tick-sampling form)';
|
|
466
481
|
var STOP_AT_FORMS = ['tick', 'ticks', 'path'];
|
|
467
482
|
var PATH_OPTIONS = ['waypoints', 'loop', 'waypointTimeoutMs'];
|
|
468
483
|
|
|
469
|
-
/**
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
484
|
+
/**
|
|
485
|
+
* THE length budget of a recording (one owner: the human form and Record.film both take it): `frames` pictures at
|
|
486
|
+
* `everyTicks` end `(frames − 1) × everyTicks` ticks after the first capture, ON the last picture — so reaching the
|
|
487
|
+
* end is truncated:false (a stop at `frames × everyTicks` would ask for one picture past the bound and report a
|
|
488
|
+
* truncation). The math of maxi-record.js::segmentTicks.
|
|
489
|
+
* @param {number} frames
|
|
490
|
+
* @param {number} everyTicks
|
|
491
|
+
* @returns {{maxFrames:number, stopAt:{ticks:number}}}
|
|
492
|
+
*/
|
|
493
|
+
function recordBudget(frames, everyTicks) {
|
|
494
|
+
return { maxFrames: frames, stopAt: { ticks: (frames - 1) * everyTicks } };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The stage/signal/encoder options both forms share, validated once. @private */
|
|
498
|
+
function _sharedRecordOptions(RS, opts, req, who) {
|
|
499
|
+
req.stage = (opts.stage === undefined) ? RS.finalStage() : RS.assertStage(opts.stage);
|
|
500
|
+
req.bitrate = (opts.bitrate === undefined) ? null : _requirePosInt(opts.bitrate, 'bitrate (bits per second)', who);
|
|
501
|
+
req.keyFrameInterval = (opts.keyFrameInterval === undefined) ? 1 : _requirePosInt(opts.keyFrameInterval, 'keyFrameInterval (frames)', who);
|
|
502
|
+
req.signal = null;
|
|
487
503
|
if (opts.signal !== undefined) {
|
|
488
504
|
var sig = opts.signal;
|
|
489
505
|
if (!sig || typeof sig.aborted !== 'boolean' || typeof sig.addEventListener !== 'function') {
|
|
490
506
|
_fail(who, 'signal must be an AbortSignal (new AbortController().signal) — it is how a recording ' +
|
|
491
|
-
'stops before its
|
|
507
|
+
'stops before its end; got ' + String(sig));
|
|
492
508
|
}
|
|
493
509
|
req.signal = sig;
|
|
494
510
|
}
|
|
@@ -498,6 +514,92 @@ function _normalizeRecord(RS, opts) {
|
|
|
498
514
|
'plausible-looking movie of nothing. Read the numbers with getStream({stage: ' +
|
|
499
515
|
JSON.stringify(req.stage) + ', everyTicks})');
|
|
500
516
|
}
|
|
517
|
+
return req;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* recordRefusal(runner) → null when the default (human) form of record() can start on this runner NOW, else
|
|
522
|
+
* `{ reason, message }`: reason 'paused' (no tick will arrive to film) or 'rate-unmeasured' (runner.getRates().tps is
|
|
523
|
+
* not yet a positive number — FrqtlApp resets it on every start and reports after its first second running). The ONE
|
|
524
|
+
* owner of when the default form refuses: record() fails with `message`, and a host that offers a Record control
|
|
525
|
+
* enables it on `null` (reading this on the live runner, never a polled copy of its rates).
|
|
526
|
+
* @param {object} runner
|
|
527
|
+
* @returns {null|{reason:string, message:string}}
|
|
528
|
+
*/
|
|
529
|
+
function recordRefusal(runner) {
|
|
530
|
+
if (!runner || typeof runner.getRates !== 'function') {
|
|
531
|
+
throw new TypeError('[FrqtlVision] recordRefusal(runner): runner is a live FrqtlApp (it reads isRunning and getRates()). (FAIL-LOUD.)');
|
|
532
|
+
}
|
|
533
|
+
if (!runner.isRunning) {
|
|
534
|
+
return { reason: 'paused', message: 'the world is paused, so no tick will arrive to film (a recording samples by ' +
|
|
535
|
+
'TICK, never by the clock) — resume first' };
|
|
536
|
+
}
|
|
537
|
+
var tps = runner.getRates().tps;
|
|
538
|
+
if (typeof tps !== 'number' || !isFinite(tps) || tps <= 0) {
|
|
539
|
+
return { reason: 'rate-unmeasured', message: 'the runner has not measured its tick rate yet (runner.getRates().tps = ' +
|
|
540
|
+
String(tps) + '; it reports after its first second running), and the default form samples at most fps pictures ' +
|
|
541
|
+
'a second of it. Record once it has run a second, or name the cadence with the tick form ' + TICK_SHAPE };
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The HUMAN form: {seconds?, fps?} — the tick cadence is derived from the live runner when recording starts. @private */
|
|
547
|
+
function _normalizeHumanRecord(RS, opts, who) {
|
|
548
|
+
var req = {
|
|
549
|
+
form: 'human',
|
|
550
|
+
seconds: (opts.seconds === undefined) ? RECORD_DEFAULTS.seconds : _requirePosInt(opts.seconds, 'seconds (playback seconds)', who),
|
|
551
|
+
fps: (opts.fps === undefined) ? RECORD_DEFAULTS.fps : _requirePosInt(opts.fps, 'fps (the PLAYBACK rate)', who),
|
|
552
|
+
everyTicks: null, stopAt: null, maxFrames: null
|
|
553
|
+
};
|
|
554
|
+
if (req.seconds > RECORD_DEFAULTS.seconds) {
|
|
555
|
+
_fail(who, 'seconds ' + req.seconds + ' is over the cap of ' + RECORD_DEFAULTS.seconds + ' s (the recording is held ' +
|
|
556
|
+
'in memory until it ends). A longer capture is the tick form: ' + TICK_SHAPE);
|
|
557
|
+
}
|
|
558
|
+
return _sharedRecordOptions(RS, opts, req, who);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The human form's request, from the LIVE runner when recording starts. Its TIME is the wall clock: every picture is
|
|
563
|
+
* stamped with its wall-clock time since the first one (_record), and the recording ends on its own once `seconds` of
|
|
564
|
+
* wall clock have been filmed — so the file lasts as long as the viewer watched, whatever the tick rate does while
|
|
565
|
+
* recording (a 1:1-locked scene slows when the recorder draws). The cadence is only a SAMPLING-RATE CEILING: the runner
|
|
566
|
+
* ticks `tps` a second (its own measured rate, runner.getRates().tps), and sampling every
|
|
567
|
+
* everyTicks = max(1, ceil(tps / fps)) ticks never films faster than `fps` at that rate (round or floor would, and a
|
|
568
|
+
* picture closer than 1/fps to the last one has to be pushed later than its wall time). The memory bound is
|
|
569
|
+
* recordBudget(seconds × fps, everyTicks).maxFrames; reaching it (the scene sped up) is truncated:true. @private
|
|
570
|
+
*/
|
|
571
|
+
function _deriveHumanRecord(runner, req) {
|
|
572
|
+
var refusal = recordRefusal(runner);
|
|
573
|
+
if (refusal) { _fail('record', refusal.message); }
|
|
574
|
+
var tps = runner.getRates().tps;
|
|
575
|
+
req.everyTicks = Math.max(1, Math.ceil(tps / req.fps));
|
|
576
|
+
req.maxFrames = recordBudget(req.seconds * req.fps, req.everyTicks).maxFrames;
|
|
577
|
+
req.stopAt = {}; // no tick stop: the wall clock ends it (_record)
|
|
578
|
+
return req;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Validate a record() request; returns the normalized request. @private */
|
|
582
|
+
function _normalizeRecord(RS, opts) {
|
|
583
|
+
var who = 'record';
|
|
584
|
+
if (opts === undefined) { opts = {}; }
|
|
585
|
+
if (!opts || typeof opts !== 'object') {
|
|
586
|
+
_fail(who, 'expected ' + HUMAN_SHAPE + ' or ' + TICK_SHAPE + ', got ' + String(opts));
|
|
587
|
+
}
|
|
588
|
+
_assertKnownKeys(opts, RECORD_OPTIONS, 'record', who);
|
|
589
|
+
var tickKeys = TICK_FORM_KEYS.filter(function (k) { return opts[k] !== undefined; });
|
|
590
|
+
if (opts.seconds !== undefined && tickKeys.length) {
|
|
591
|
+
_fail(who, 'a call mixes the two shapes (seconds with ' + tickKeys.join(', ') + '). Pass EITHER ' + HUMAN_SHAPE +
|
|
592
|
+
' OR ' + TICK_SHAPE);
|
|
593
|
+
}
|
|
594
|
+
if (!tickKeys.length) { return _normalizeHumanRecord(RS, opts, who); }
|
|
595
|
+
var req = {
|
|
596
|
+
form: 'tick',
|
|
597
|
+
everyTicks: _requirePosInt(opts.everyTicks, 'everyTicks (the capture cadence in TICKS — there is no wall-clock cadence)', who),
|
|
598
|
+
maxFrames: _requirePosInt(opts.maxFrames, 'maxFrames (the memory bound; hitting it reports truncated:true)', who),
|
|
599
|
+
fps: _requirePosInt(opts.fps, 'fps (the PLAYBACK rate — it has no physical meaning)', who),
|
|
600
|
+
stopAt: null
|
|
601
|
+
};
|
|
602
|
+
_sharedRecordOptions(RS, opts, req, who);
|
|
501
603
|
var s = opts.stopAt;
|
|
502
604
|
if (!s || typeof s !== 'object') {
|
|
503
605
|
_fail(who, 'stopAt is REQUIRED — exactly one of {tick: T}, {ticks: K}, {path: {waypoints, loop?, ' +
|
|
@@ -575,6 +677,8 @@ function _record(runner, RS, opts) {
|
|
|
575
677
|
'localhost). The encoder is not optional — there is no second path');
|
|
576
678
|
}
|
|
577
679
|
|
|
680
|
+
if (req.form === 'human') { _deriveHumanRecord(runner, req); }
|
|
681
|
+
|
|
578
682
|
// THE CLIP HOOK: while a Record is open on this runner, every recording is one of its clips. The Record
|
|
579
683
|
// refuses a request that cannot be a CLIP_SECONDS clip (before anything subscribes) and keeps the clip's
|
|
580
684
|
// ticks and bytes; this recorder holds no Record state of its own (objects/record/Record.js _clipFor).
|
|
@@ -600,6 +704,12 @@ function _record(runner, RS, opts) {
|
|
|
600
704
|
var closing = false;
|
|
601
705
|
var failure = null;
|
|
602
706
|
var stopTick = (req.stopAt.tick !== undefined) ? req.stopAt.tick : null;
|
|
707
|
+
// The human form's clock: picture i's wall time since picture 0 as a SLOT of the 1/fps grid the muxer writes
|
|
708
|
+
// (whole numbers, so two pictures can never share a slot: a picture landing in or before the last one's slot
|
|
709
|
+
// takes the next), and whether it is the last one the `seconds` of wall clock admit.
|
|
710
|
+
var human = (req.form === 'human');
|
|
711
|
+
var wallT0 = null;
|
|
712
|
+
var slotOf = [];
|
|
603
713
|
var session = null;
|
|
604
714
|
var onAbort = null;
|
|
605
715
|
var dims = null;
|
|
@@ -685,6 +795,14 @@ function _record(runner, RS, opts) {
|
|
|
685
795
|
},
|
|
686
796
|
onStill: function (p, i) {
|
|
687
797
|
if (clip) { clip.sample(i); }
|
|
798
|
+
var lastByWall = false;
|
|
799
|
+
if (human) {
|
|
800
|
+
var now = performance.now();
|
|
801
|
+
if (i === 0) { wallT0 = now; }
|
|
802
|
+
var slot = Math.round((now - wallT0) / 1000 * req.fps);
|
|
803
|
+
slotOf[i] = (i === 0) ? 0 : Math.max(slot, slotOf[i - 1] + 1);
|
|
804
|
+
lastByWall = slotOf[i] + 1 >= req.seconds * req.fps;
|
|
805
|
+
}
|
|
688
806
|
var handled = p.then(function (still) {
|
|
689
807
|
if (i === 0) {
|
|
690
808
|
scene = still.scene;
|
|
@@ -699,7 +817,7 @@ function _record(runner, RS, opts) {
|
|
|
699
817
|
encodeChain = encodeChain.then(function (enc) {
|
|
700
818
|
var sample = new enc.MB.VideoSample(still.data, {
|
|
701
819
|
format: 'RGBA', codedWidth: still.width, codedHeight: still.height,
|
|
702
|
-
timestamp: i / req.fps, duration: 1 / req.fps
|
|
820
|
+
timestamp: (human ? slotOf[i] : i) / req.fps, duration: 1 / req.fps
|
|
703
821
|
});
|
|
704
822
|
return Promise.resolve(enc.source.add(sample)).then(function () {
|
|
705
823
|
sample.close();
|
|
@@ -709,6 +827,7 @@ function _record(runner, RS, opts) {
|
|
|
709
827
|
// Handled at close; a rejected encode must not surface as unhandled meanwhile.
|
|
710
828
|
encodeChain.then(null, function (e) { close(e); });
|
|
711
829
|
if (stopTick !== null && still.tick + req.everyTicks > stopTick) { close(null); }
|
|
830
|
+
if (lastByWall) { close(null); }
|
|
712
831
|
}, function (e) { close(e); });
|
|
713
832
|
inflight.push(handled);
|
|
714
833
|
return handled;
|
|
@@ -787,8 +906,19 @@ function attach(runner) {
|
|
|
787
906
|
*/
|
|
788
907
|
getStream: function (opts) { return _getStream(runner, RS, opts); },
|
|
789
908
|
/**
|
|
790
|
-
* record(opts) → Promise<VideoEnvelope> —
|
|
909
|
+
* record(opts?) → Promise<VideoEnvelope> — film what the viewer sees, encode an MP4.
|
|
910
|
+
*
|
|
911
|
+
* THE DEFAULT (human) FORM — record({seconds?, fps?, signal?, stage?, bitrate?, keyFrameInterval?}):
|
|
912
|
+
* seconds default and cap RECORD_DEFAULTS.seconds (60) — the recording ends on its own
|
|
913
|
+
* after this many seconds of WALL CLOCK (truncated:false)
|
|
914
|
+
* fps default RECORD_DEFAULTS.fps (30) — the most pictures a second it films
|
|
915
|
+
* signal an AbortSignal: abort is Stop — what was filmed, truncated:true
|
|
916
|
+
* Each picture is stamped with its wall-clock time since the first, so the file lasts as long as the
|
|
917
|
+
* viewer watched whatever the tick rate does; picture i is NOT tick i / fps in this form (the tick form
|
|
918
|
+
* keeps that). Refused on a paused runner, or before the runner has measured its rate (FrqtlVision.recordRefusal).
|
|
791
919
|
*
|
|
920
|
+
* THE ADVANCED (tick) FORM — any of everyTicks/stopAt/maxFrames selects it; mixing it with
|
|
921
|
+
* `seconds` throws naming both shapes:
|
|
792
922
|
* everyTicks REQUIRED — capture cadence in TICKS (sampling starts at the first
|
|
793
923
|
* tick after this call, then every `everyTicks`)
|
|
794
924
|
* stopAt REQUIRED — exactly one of `{tick: T}` (absolute, inclusive),
|
|
@@ -798,7 +928,7 @@ function attach(runner) {
|
|
|
798
928
|
* waypoint)
|
|
799
929
|
* maxFrames REQUIRED — the memory bound; reaching it before `stopAt` stops the
|
|
800
930
|
* recording and reports `truncated: true`
|
|
801
|
-
* fps REQUIRED — PLAYBACK rate only; frame i is timestamped i/fps
|
|
931
|
+
* fps REQUIRED — PLAYBACK rate only; frame i is timestamped i/fps (tick-exact timing)
|
|
802
932
|
* stage default `finalStage()`; must be an rgba8 stage
|
|
803
933
|
* signal an AbortSignal: abort ends the recording early with the stills
|
|
804
934
|
* captured so far and `truncated: true` (rejects if none yet)
|
|
@@ -823,7 +953,8 @@ function attach(runner) {
|
|
|
823
953
|
|
|
824
954
|
// ── Export ────────────────────────────────────────────────────────────────────
|
|
825
955
|
|
|
826
|
-
var FrqtlVision = { attach: attach, toDataURL: toDataURL, toPNG: toPNG
|
|
956
|
+
var FrqtlVision = { attach: attach, toDataURL: toDataURL, toPNG: toPNG, RECORD_DEFAULTS: RECORD_DEFAULTS, recordBudget: recordBudget,
|
|
957
|
+
recordRefusal: recordRefusal };
|
|
827
958
|
|
|
828
959
|
// The capture-host KIND's in-page recorder is THIS face: registered here, so capture-host
|
|
829
960
|
// never names FrqtlVision (the import runs one way: FrqtlVision → capture-host).
|
|
@@ -76,7 +76,7 @@ import { registerKind } from '../objects/kinds.js';
|
|
|
76
76
|
'source fails loud.',
|
|
77
77
|
// STUDIO UX: a chart panel has a real draggable/resizable extent, and it is
|
|
78
78
|
// VISIBLE during Run (its whole point), so shellRender:'run' (NOT a Design-only
|
|
79
|
-
// marker).
|
|
79
|
+
// marker).
|
|
80
80
|
ux: {
|
|
81
81
|
resizable: true,
|
|
82
82
|
draggable: { '2d': true },
|
|
@@ -617,8 +617,8 @@ import { CollisionTypes } from './CollisionTypes.js';
|
|
|
617
617
|
// UX_DRAGGABLE_MODES / UX_SHELL_RENDER); this block is read via
|
|
618
618
|
// ObjectSchema.ux('SphereEmitter'). resizable:false — a SphereEmitter has
|
|
619
619
|
// TWO radii (inner+outer); a single-box resize gesture is ambiguous, so
|
|
620
|
-
// it is NOT resizable (individually, per object).
|
|
621
|
-
//
|
|
620
|
+
// it is NOT resizable (individually, per object). handles: ['move'] — the
|
|
621
|
+
// Hand only moves it (2D and 3D), never scales it.
|
|
622
622
|
// shellRender:'design' — the marker paints only while authoring (the
|
|
623
623
|
// default; the "red light bulb" flips a per-instance shellRender override).
|
|
624
624
|
ux: {
|
|
@@ -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 },
|