@displayxr/inline3d 1.38.0 → 1.40.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/CHANGELOG.md +162 -0
- package/call.d.ts +9 -1
- package/camera.d.ts +17 -0
- package/dist/call.js +4 -4
- package/index.d.ts +155 -18
- package/js/call/element.js +2 -0
- package/js/call/options.js +4 -0
- package/js/camera/pair-cursor.js +166 -0
- package/js/inline3d-call.js +28 -4
- package/js/inline3d-camera.js +30 -4
- package/js/inline3d-cursor-option.js +18 -0
- package/js/inline3d-model-entry.js +7 -0
- package/js/inline3d-model-playcanvas.js +5 -0
- package/js/inline3d-model.js +12 -0
- package/js/inline3d-splat-pc-entry.js +7 -1
- package/js/inline3d-splat-playcanvas.js +105 -4
- package/js/inline3d-splat-shared.js +17 -0
- package/js/inline3d-splat.js +12 -0
- package/js/inline3d-viewer.js +35 -5
- package/js/inline3d.js +667 -17
- package/model.d.ts +8 -1
- package/package.json +1 -1
- package/splat.d.ts +8 -1
|
@@ -36,3 +36,21 @@ export function resolveCursorOption(opt, who = '@displayxr/inline3d') {
|
|
|
36
36
|
}
|
|
37
37
|
return { ...opt };
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** The keys `cursor: { … }` accepts on a stereo PAIR (./camera's addCameraView, ./call's tiles). */
|
|
41
|
+
export const PAIR_CURSOR_OPTION_KEYS = Object.freeze(['height', 'pointerScope']);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `cursor` on a stereo pair — a camera view or a call tile: `'depth'`, or `{ height, pointerScope }`.
|
|
45
|
+
* A pair has no scene: its depth is MEASURED off the two halves, and the sprite is drawn into them,
|
|
46
|
+
* so there is no anchor mode (it is always on the line of sight) and the margin is the SDK's. Falsy
|
|
47
|
+
* is OFF, the zero-cost default.
|
|
48
|
+
* @returns {null | {height?: number, pointerScope?: string}}
|
|
49
|
+
*/
|
|
50
|
+
export function resolvePairCursorOption(opt, who = '@displayxr/inline3d') {
|
|
51
|
+
const o = resolveCursorOption(opt, who);
|
|
52
|
+
if (!o) return null;
|
|
53
|
+
const unknown = Object.keys(o).filter((k) => !PAIR_CURSOR_OPTION_KEYS.includes(k));
|
|
54
|
+
if (unknown.length) throw new Error(`${who}: cursor — ${unknown.join(', ')} not supported on a stereo pair; expected ${PAIR_CURSOR_OPTION_KEYS.join(', ')}.`);
|
|
55
|
+
return o;
|
|
56
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// engine import fails, one warning names what to install, and the tile renders on three. With
|
|
22
22
|
// neither, `ready` rejects naming both. An explicit `engine` never falls back.
|
|
23
23
|
|
|
24
|
+
import { guardedAttach } from './inline3d-splat-shared.js';
|
|
24
25
|
/** The engines `addModel` renders with; the first is the default. */
|
|
25
26
|
export const MODEL_ENGINES = Object.freeze(['playcanvas', 'three']);
|
|
26
27
|
|
|
@@ -98,6 +99,12 @@ export function validateModelCall(canvas, src, opts, route) {
|
|
|
98
99
|
* fill in as the backend loads.
|
|
99
100
|
*/
|
|
100
101
|
export function addModel(wall, canvas, src, opts = {}) {
|
|
102
|
+
// A second call on a canvas still registered on this wall warns once, then proceeds as before
|
|
103
|
+
// (see guardedAttach).
|
|
104
|
+
return guardedAttach(wall, canvas, 'addModel', opts, (o) => addModelUnguarded(wall, canvas, src, o));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addModelUnguarded(wall, canvas, src, opts = {}) {
|
|
101
108
|
const route = resolveModelEngine(opts);
|
|
102
109
|
validateModelCall(canvas, src, opts, route);
|
|
103
110
|
if (route.engine === 'playcanvas') {
|
|
@@ -531,6 +531,11 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
531
531
|
bufferClamped: true, // this viewer clamps its own buffer; the core must never touch its context
|
|
532
532
|
...(observe ? { observe } : {}),
|
|
533
533
|
...(firstWovenHoldMs !== undefined ? { firstWovenHoldMs } : {}),
|
|
534
|
+
// The SDK covers this canvas across a move/resize (core rectCover): on by default here, the
|
|
535
|
+
// preview tier; 'off' for a page that runs its own cover.
|
|
536
|
+
rectCover: opts.rectCover === undefined ? 'auto' : opts.rectCover,
|
|
537
|
+
// The entry's double-attach claim (guardedAttach): lets THIS registration through.
|
|
538
|
+
_dxrClaim: opts._dxrClaim,
|
|
534
539
|
});
|
|
535
540
|
} else {
|
|
536
541
|
viewer.startMono();
|
package/js/inline3d-model.js
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
// your own origin (`/draco/`, `/basis/`) and `opts.decoderPath` moves that. See
|
|
39
39
|
// docs/authoring-inline-3d.md#compressed-gltf.
|
|
40
40
|
|
|
41
|
+
import { guardedAttach } from './inline3d-splat-shared.js';
|
|
41
42
|
import * as THREE from 'three';
|
|
42
43
|
import { EyeCamera, EdgeFeather, DepthCursor, raycastHitTest } from './inline3d-three.js';
|
|
43
44
|
import { resolveCursorOption } from './inline3d-cursor-depth.js';
|
|
@@ -345,6 +346,12 @@ function kindFromMessage(msg) {
|
|
|
345
346
|
* `setPose`, `resetPose`, `frame`, and `ready`.
|
|
346
347
|
*/
|
|
347
348
|
export function addModel(wall, canvas, src, opts = {}) {
|
|
349
|
+
// A second call on a canvas still registered on this wall warns once, then proceeds as before
|
|
350
|
+
// (see guardedAttach).
|
|
351
|
+
return guardedAttach(wall, canvas, 'addModel', opts, (o) => addModelUnguarded(wall, canvas, src, o));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function addModelUnguarded(wall, canvas, src, opts = {}) {
|
|
348
355
|
const {
|
|
349
356
|
virtualDisplayHeight = 0.24,
|
|
350
357
|
frame = null,
|
|
@@ -426,6 +433,11 @@ export function addModel(wall, canvas, src, opts = {}) {
|
|
|
426
433
|
bufferClamped: true, // this viewer clamps its own buffer; the core must never touch its context
|
|
427
434
|
...(observe ? { observe } : {}),
|
|
428
435
|
...(firstWovenHoldMs !== undefined ? { firstWovenHoldMs } : {}),
|
|
436
|
+
// The SDK covers this canvas across a move/resize (core rectCover): on by default here, the
|
|
437
|
+
// preview tier; 'off' for a page that runs its own cover.
|
|
438
|
+
rectCover: opts.rectCover === undefined ? 'auto' : opts.rectCover,
|
|
439
|
+
// The entry's double-attach claim (guardedAttach): lets THIS registration through.
|
|
440
|
+
_dxrClaim: opts._dxrClaim,
|
|
429
441
|
});
|
|
430
442
|
} else {
|
|
431
443
|
viewer.startMono();
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Not here: measureSplatBounds (it measures a Spark mesh — import it from ./splat).
|
|
17
17
|
|
|
18
18
|
import { resolveRevealOption } from './inline3d-splat-effects.js';
|
|
19
|
-
import { CAPTURE_FITS, playcanvasCannotRead, resolveControls } from './inline3d-splat-shared.js';
|
|
19
|
+
import { CAPTURE_FITS, guardedAttach, playcanvasCannotRead, resolveControls } from './inline3d-splat-shared.js';
|
|
20
20
|
import { addSplatDeferred } from './inline3d-splat-deferred.js';
|
|
21
21
|
|
|
22
22
|
export { applySplatPerf, SPLAT_PERF_PRESETS } from './inline3d-splat-perf.js';
|
|
@@ -25,6 +25,12 @@ export { resolveRig } from './inline3d-splat-rig.js';
|
|
|
25
25
|
|
|
26
26
|
/** addSplat with `engine: 'playcanvas'`; the same validation, at the call, as ./splat's. */
|
|
27
27
|
export function addSplat(wall, canvas, src, opts = {}) {
|
|
28
|
+
// A second call on a canvas still registered on this wall warns once, then proceeds as before
|
|
29
|
+
// (see guardedAttach).
|
|
30
|
+
return guardedAttach(wall, canvas, 'addSplat', opts, (o) => addSplatUnguarded(wall, canvas, src, o));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function addSplatUnguarded(wall, canvas, src, opts = {}) {
|
|
28
34
|
if (opts.engine !== undefined && opts.engine !== 'playcanvas') {
|
|
29
35
|
throw new Error(
|
|
30
36
|
`@displayxr/inline3d/splat/playcanvas: engine "${opts.engine}" — this entry is PlayCanvas ` +
|
|
@@ -1002,7 +1002,11 @@ export class PlayCanvasSplatViewer {
|
|
|
1002
1002
|
this._tileBudget = undefined; // undefined = the engine's own default
|
|
1003
1003
|
this._budgetViews = 1;
|
|
1004
1004
|
|
|
1005
|
-
|
|
1005
|
+
// Resize AND draw inside the observer callback (it runs after the frame's animation callbacks
|
|
1006
|
+
// and before the paint). Deferring to the next animation frame, as this used to, let one
|
|
1007
|
+
// paint show the old backing store stretched onto the new box: a visible jolt on every
|
|
1008
|
+
// CSS resize of a woven canvas. See _resizeNow for the two cases that still defer.
|
|
1009
|
+
this._onResize = () => this._resizeNow();
|
|
1006
1010
|
this._ro = typeof ResizeObserver === 'function' ? new ResizeObserver(this._onResize) : null;
|
|
1007
1011
|
if (this._ro) this._ro.observe(canvas);
|
|
1008
1012
|
else if (typeof addEventListener === 'function') addEventListener('resize', this._onResize);
|
|
@@ -1231,7 +1235,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1231
1235
|
this._beforeFrame?.(views || null);
|
|
1232
1236
|
this._tick();
|
|
1233
1237
|
if (!views || views.length < 2) {
|
|
1234
|
-
this.
|
|
1238
|
+
this._replayOrFlat();
|
|
1235
1239
|
return;
|
|
1236
1240
|
}
|
|
1237
1241
|
const vps = this._vps;
|
|
@@ -1239,7 +1243,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1239
1243
|
for (const view of views) {
|
|
1240
1244
|
const vp = layer && typeof layer.getViewport === 'function' ? layer.getViewport(view) : null;
|
|
1241
1245
|
if (!vp || !(vp.width > 0) || !(vp.height > 0)) {
|
|
1242
|
-
this.
|
|
1246
|
+
this._replayOrFlat();
|
|
1243
1247
|
return;
|
|
1244
1248
|
}
|
|
1245
1249
|
vps.push(vp);
|
|
@@ -2010,6 +2014,17 @@ export class PlayCanvasSplatViewer {
|
|
|
2010
2014
|
* engine tick. Every entry is {proj, pose, x, y, width, height} in BUFFER pixels.
|
|
2011
2015
|
*/
|
|
2012
2016
|
_drawEntries(entries, cache) {
|
|
2017
|
+
// `_drawing` is what _resizeNow checks: never reallocate the buffer under a running render.
|
|
2018
|
+
if (this._drawing) return this._drawEntriesInner(entries, cache);
|
|
2019
|
+
this._drawing = true;
|
|
2020
|
+
try {
|
|
2021
|
+
return this._drawEntriesInner(entries, cache);
|
|
2022
|
+
} finally {
|
|
2023
|
+
this._drawing = false;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
_drawEntriesInner(entries, cache) {
|
|
2013
2028
|
const app = this.app;
|
|
2014
2029
|
if (!app || !this.pc) return false;
|
|
2015
2030
|
const pc = this.pc;
|
|
@@ -2139,6 +2154,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2139
2154
|
}
|
|
2140
2155
|
g.bufW = el.width || 0;
|
|
2141
2156
|
g.bufH = el.height || 0;
|
|
2157
|
+
g.boxAspect = this.boxAspect || 0; // the CSS shape these views were located for (see _replayLastGood)
|
|
2142
2158
|
g.rig = this._rigSnap ? { ...this._rigSnap } : null; // a replay re-uses the rig of its views
|
|
2143
2159
|
g.rigAt = this._rigAtPull ?? null;
|
|
2144
2160
|
for (let i = 0; i < views.length; i++) {
|
|
@@ -2153,10 +2169,90 @@ export class PlayCanvasSplatViewer {
|
|
|
2153
2169
|
}
|
|
2154
2170
|
}
|
|
2155
2171
|
|
|
2172
|
+
/**
|
|
2173
|
+
* A session frame that cannot draw its own views still PRESENTS: the last good frame, or —
|
|
2174
|
+
* before there is one — the mono camera into both halves (flat, the subject if loaded, else
|
|
2175
|
+
* the clear colour). A woven canvas that is not redrawn can drop out of the frame the
|
|
2176
|
+
* compositor aggregates (woven-canvas-rules, "redraw every frame"), and a load-fallback frame
|
|
2177
|
+
* before the first stereo one is exactly when no cache exists. The only frame this cannot
|
|
2178
|
+
* cover is one before the engine has attached: there is no GL context yet, and creating one
|
|
2179
|
+
* here would create it with the wrong attributes.
|
|
2180
|
+
*/
|
|
2181
|
+
_replayOrFlat() {
|
|
2182
|
+
return this._replayLastGood() || this._drawFlatPair();
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
_drawFlatPair() {
|
|
2186
|
+
if (this._disposed || !this.app || !this.pc) return false;
|
|
2187
|
+
const b = this._bufScale();
|
|
2188
|
+
const W = b.w || this.canvas.width;
|
|
2189
|
+
const H = b.h || this.canvas.height;
|
|
2190
|
+
if (!(W > 1) || !(H > 0)) return false;
|
|
2191
|
+
const half = Math.floor(W / 2);
|
|
2192
|
+
const v = this._monoView();
|
|
2193
|
+
const pair = (this._flatPair ||= [0, 1].map(() => ({ proj: null, pose: null, x: 0, y: 0, width: 0, height: 0 })));
|
|
2194
|
+
for (let i = 0; i < 2; i++) {
|
|
2195
|
+
const e = pair[i];
|
|
2196
|
+
e.proj = v.proj;
|
|
2197
|
+
e.pose = v.pose;
|
|
2198
|
+
e.x = i * half;
|
|
2199
|
+
e.y = 0;
|
|
2200
|
+
e.width = half;
|
|
2201
|
+
e.height = H;
|
|
2202
|
+
}
|
|
2203
|
+
return this._drawEntries(pair, null);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2156
2206
|
_replayLastGood() {
|
|
2157
2207
|
const g = this._lastGood;
|
|
2158
2208
|
if (!g || this._disposed) return false;
|
|
2159
|
-
|
|
2209
|
+
// A box whose aspect changed since the cache (a CSS resize replayed before the runtime has
|
|
2210
|
+
// located views for the new rect): the cached frustum was built for the OLD shape, and the
|
|
2211
|
+
// viewports map onto the new buffer proportionally, so the picture would be stretched by
|
|
2212
|
+
// exactly the aspect ratio change. Widen (or narrow) the frustum by the same factor —
|
|
2213
|
+
// dividing the projection's x row by it — so the replayed frame keeps its proportions. One
|
|
2214
|
+
// frame's approximation: the next located views carry the true frustum for the new window.
|
|
2215
|
+
const f = g.boxAspect > 0 && this.boxAspect > 0 ? this.boxAspect / g.boxAspect : 1;
|
|
2216
|
+
if (Math.abs(f - 1) < 1e-6) return this._drawEntries(g.entries, g);
|
|
2217
|
+
const fixed = (this._aspectFixed ||= []);
|
|
2218
|
+
fixed.length = g.entries.length;
|
|
2219
|
+
for (let i = 0; i < g.entries.length; i++) {
|
|
2220
|
+
const e = g.entries[i];
|
|
2221
|
+
const o = (fixed[i] ||= { proj: new Float32Array(16), pose: null, x: 0, y: 0, width: 0, height: 0 });
|
|
2222
|
+
o.proj.set(e.proj);
|
|
2223
|
+
o.proj[0] /= f;
|
|
2224
|
+
o.proj[4] /= f;
|
|
2225
|
+
o.proj[8] /= f;
|
|
2226
|
+
o.proj[12] /= f;
|
|
2227
|
+
o.pose = e.pose;
|
|
2228
|
+
o.x = e.x;
|
|
2229
|
+
o.y = e.y;
|
|
2230
|
+
o.width = e.width;
|
|
2231
|
+
o.height = e.height;
|
|
2232
|
+
}
|
|
2233
|
+
return this._drawEntries(fixed, g);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* The observer's resize, synchronous. Two cases still take the debounced path instead: a call
|
|
2238
|
+
* that arrives while this viewer is already resizing (re-entrancy: setting canvas.width can
|
|
2239
|
+
* queue more observer work), and one that arrives while it is drawing (a nested dispatch from
|
|
2240
|
+
* inside the engine's tick). Reallocating the drawing buffer under a running render would
|
|
2241
|
+
* leave that render half on a cleared store.
|
|
2242
|
+
*/
|
|
2243
|
+
_resizeNow() {
|
|
2244
|
+
if (this._disposed) return;
|
|
2245
|
+
if (this._inResize || this._drawing) {
|
|
2246
|
+
this._scheduleResize();
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
this._resizePending = false; // this run supersedes a debounced one already queued
|
|
2250
|
+
this._inResize = true;
|
|
2251
|
+
try {
|
|
2252
|
+
this._resize();
|
|
2253
|
+
} finally {
|
|
2254
|
+
this._inResize = false;
|
|
2255
|
+
}
|
|
2160
2256
|
}
|
|
2161
2257
|
|
|
2162
2258
|
_scheduleResize() {
|
|
@@ -3599,6 +3695,11 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3599
3695
|
bufferClamped: true, // this viewer clamps its own buffer; the core must never touch its context
|
|
3600
3696
|
...(observe ? { observe } : {}),
|
|
3601
3697
|
...(firstWovenHoldMs !== undefined ? { firstWovenHoldMs } : {}),
|
|
3698
|
+
// The SDK covers this canvas across a move/resize (core rectCover): on by default here, the
|
|
3699
|
+
// preview tier; 'off' for a page that runs its own cover.
|
|
3700
|
+
rectCover: opts.rectCover === undefined ? 'auto' : opts.rectCover,
|
|
3701
|
+
// The entry's double-attach claim (guardedAttach): lets THIS registration through.
|
|
3702
|
+
_dxrClaim: opts._dxrClaim,
|
|
3602
3703
|
});
|
|
3603
3704
|
} else {
|
|
3604
3705
|
viewer.startMono();
|
|
@@ -12,6 +12,23 @@
|
|
|
12
12
|
// apart on how a drag, a wheel notch, an idle turntable or a focus change feels. Pinned by
|
|
13
13
|
// test/splat-playcanvas.test.mjs (values, and a behavioural trace of both viewers side by side).
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Double-attach detection around a scene subpath's public entry (`addSplat`, `addModel`).
|
|
17
|
+
*
|
|
18
|
+
* The check has to run HERE, at the entry, and not only in the core's addScene: the subpaths build
|
|
19
|
+
* a renderer on the canvas BEFORE the core window exists (asynchronously, once the engine module
|
|
20
|
+
* has loaded), so a second `addSplat` during the first one's load would never reach the core's
|
|
21
|
+
* check in time. `wall._attachGuard` (the core manager) warns once per canvas and claims it; the
|
|
22
|
+
* call then proceeds exactly as before. A wall without it (no session, an older core) attaches as is.
|
|
23
|
+
*/
|
|
24
|
+
export function guardedAttach(wall, canvas, method, opts, attach) {
|
|
25
|
+
const g = wall && typeof wall._attachGuard === 'function' ? wall._attachGuard(canvas, method, opts) : null;
|
|
26
|
+
if (!g) return attach(opts);
|
|
27
|
+
const out = attach(g.opts);
|
|
28
|
+
g.own(out);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
/**
|
|
16
33
|
* Backstop on total subject depth, as a multiple of the display height. Generous on purpose:
|
|
17
34
|
* depth placement is a z decision (see fitTo), not a scale one, so this only catches the
|
package/js/inline3d-splat.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
STREAMED_NEEDS_PLAYCANVAS,
|
|
40
40
|
resolveControls,
|
|
41
41
|
declareViewRig,
|
|
42
|
+
guardedAttach,
|
|
42
43
|
} from './inline3d-splat-shared.js';
|
|
43
44
|
import {
|
|
44
45
|
resolveRig,
|
|
@@ -162,6 +163,12 @@ function sniffFileType(bytes) {
|
|
|
162
163
|
* good enough for a clean, isolated capture, weaker on a scene with a background wall.
|
|
163
164
|
*/
|
|
164
165
|
export function addSplat(wall, canvas, src, opts = {}) {
|
|
166
|
+
// A second call on a canvas still registered on this wall warns once, then proceeds as before
|
|
167
|
+
// (see guardedAttach).
|
|
168
|
+
return guardedAttach(wall, canvas, 'addSplat', opts, (o) => addSplatUnguarded(wall, canvas, src, o));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function addSplatUnguarded(wall, canvas, src, opts = {}) {
|
|
165
172
|
if (opts.captureFit !== undefined && !CAPTURE_FITS.includes(opts.captureFit)) {
|
|
166
173
|
throw new Error(
|
|
167
174
|
`@displayxr/inline3d/splat: captureFit "${opts.captureFit}" — expected ` +
|
|
@@ -487,6 +494,11 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
487
494
|
bufferClamped: true, // this viewer clamps its own buffer; the core must never touch its context
|
|
488
495
|
...(observe ? { observe } : {}),
|
|
489
496
|
...(firstWovenHoldMs !== undefined ? { firstWovenHoldMs } : {}),
|
|
497
|
+
// The SDK covers this canvas across a move/resize (core rectCover): on by default here, the
|
|
498
|
+
// preview tier; 'off' for a page that runs its own cover.
|
|
499
|
+
rectCover: opts.rectCover === undefined ? 'auto' : opts.rectCover,
|
|
500
|
+
// The entry's double-attach claim (guardedAttach): lets THIS registration through.
|
|
501
|
+
_dxrClaim: opts._dxrClaim,
|
|
490
502
|
});
|
|
491
503
|
} else {
|
|
492
504
|
viewer.startMono();
|
package/js/inline3d-viewer.js
CHANGED
|
@@ -699,7 +699,7 @@ export class SceneViewer {
|
|
|
699
699
|
|
|
700
700
|
// 1. A short view list is the load-induced mono fallback. Stereo needs two.
|
|
701
701
|
if (!views || views.length < 2) {
|
|
702
|
-
this.
|
|
702
|
+
this._replayOrFlat();
|
|
703
703
|
return;
|
|
704
704
|
}
|
|
705
705
|
|
|
@@ -725,7 +725,7 @@ export class SceneViewer {
|
|
|
725
725
|
for (const view of views) {
|
|
726
726
|
const vp = layer && typeof layer.getViewport === 'function' ? layer.getViewport(view) : null;
|
|
727
727
|
if (!vp || !(vp.width > 0) || !(vp.height > 0)) {
|
|
728
|
-
this.
|
|
728
|
+
this._replayOrFlat();
|
|
729
729
|
return;
|
|
730
730
|
}
|
|
731
731
|
vps.push(vp);
|
|
@@ -891,11 +891,41 @@ export class SceneViewer {
|
|
|
891
891
|
}
|
|
892
892
|
}
|
|
893
893
|
|
|
894
|
+
/**
|
|
895
|
+
* A session frame that cannot draw its own views still PRESENTS (1.38): the last good frame,
|
|
896
|
+
* or, before there is one, the mono camera into both halves — flat, but drawn. An undrawn
|
|
897
|
+
* woven canvas can drop out of the frame the compositor aggregates (woven-canvas-rules,
|
|
898
|
+
* "redraw every frame"), and this viewer used to leave exactly the frames before the first
|
|
899
|
+
* stereo one undrawn. Every clear here is followed by a draw, so the dark-blink rule (web#12)
|
|
900
|
+
* still holds.
|
|
901
|
+
*/
|
|
902
|
+
_replayOrFlat() {
|
|
903
|
+
return this._replayLastGood() || this._drawFlatPair();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
_drawFlatPair() {
|
|
907
|
+
if (this._disposed) return false;
|
|
908
|
+
const r = this.renderer;
|
|
909
|
+
const b = this._bufScale();
|
|
910
|
+
if (!(b.w > 1) || !(b.h > 0)) return false;
|
|
911
|
+
const half = Math.floor(b.w / 2);
|
|
912
|
+
r.clear();
|
|
913
|
+
r.setScissorTest(true);
|
|
914
|
+
for (const x of [0, half]) {
|
|
915
|
+
const vp = { x, y: 0, width: half, height: b.h };
|
|
916
|
+
r.setViewport(vp.x, vp.y, vp.width, vp.height);
|
|
917
|
+
r.setScissor(vp.x, vp.y, vp.width, vp.height);
|
|
918
|
+
r.render(this.scene, this.monoCamera);
|
|
919
|
+
if (this._feather) this._feather.render(r, vp);
|
|
920
|
+
}
|
|
921
|
+
r.setScissorTest(false);
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
|
|
894
925
|
/**
|
|
895
926
|
* Re-render the last good frame from the cached matrices. Returns false when there is no
|
|
896
|
-
* cache yet
|
|
897
|
-
*
|
|
898
|
-
* of which are better than black.
|
|
927
|
+
* cache yet (onFrame then draws the flat pair, see _replayOrFlat; a resize leaves the
|
|
928
|
+
* freshly cleared store to the next session frame).
|
|
899
929
|
*/
|
|
900
930
|
_replayLastGood() {
|
|
901
931
|
const g = this._lastGood;
|