@displayxr/inline3d 1.24.0 → 1.25.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 +117 -0
- package/README.md +15 -0
- package/index.d.ts +57 -0
- package/js/inline3d-player.js +2219 -0
- package/js/inline3d-splat-layer-rig.js +341 -125
- package/js/inline3d-splat-playcanvas.js +35 -19
- package/js/inline3d-splat.js +8 -1
- package/js/inline3d.js +208 -12
- package/package.json +7 -1
- package/player.d.ts +273 -0
- package/splat.d.ts +38 -7
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
} from './inline3d-splat-effects.js';
|
|
69
69
|
import { LiveOutgoing, resolveOutgoingOption, defaultOutgoing, yieldIdle } from './inline3d-splat-live.js';
|
|
70
70
|
import { VideoPlane, validateSetVideo, PAGE_VIDEO_ERROR, eyeSplit, EYE_SPLIT_UNIFORM, makeSbsMaterial } from './inline3d-splat-video.js';
|
|
71
|
-
import { LayerRigCameras, validateLayerRig } from './inline3d-splat-layer-rig.js';
|
|
71
|
+
import { LayerRigCameras, validateLayerRig, validateLayerRigOptions, DEFAULT_VIEWER_DISTANCE_M } from './inline3d-splat-layer-rig.js';
|
|
72
72
|
import { RigTracker, remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
|
|
73
73
|
import { resolveDiag, DiagRecorder, startDiagLoop, registerDiag, DIAG_SWITCHES } from './inline3d-splat-diag.js';
|
|
74
74
|
import {
|
|
@@ -1940,8 +1940,11 @@ export class PlayCanvasSplatViewer {
|
|
|
1940
1940
|
// setSource's wavefront: this frame's eye views, as the engine is about to compose them.
|
|
1941
1941
|
this.onBeforeRender?.(entries, rect);
|
|
1942
1942
|
// handle.setLayerRig: the display / post run cameras, on the same views (display: rounded).
|
|
1943
|
-
if (this.layerRigs
|
|
1944
|
-
this.layerRigs.frame(eyeViews ? rf.cull : entries, rect, f, eyeViews ? rf.eye.rig : rf.located ? rf.located.rig : cache ? cache.rig : null
|
|
1943
|
+
if (this.layerRigs) {
|
|
1944
|
+
this.layerRigs.frame(eyeViews ? rf.cull : entries, rect, f, eyeViews ? rf.eye.rig : rf.located ? rf.located.rig : cache ? cache.rig : null, {
|
|
1945
|
+
located: this.rigTrack && entries.length > 1 ? !!rf.located : null,
|
|
1946
|
+
residual: this.rigTrack?.lastResidual ?? null,
|
|
1947
|
+
});
|
|
1945
1948
|
}
|
|
1946
1949
|
} else {
|
|
1947
1950
|
// Fallback: one camera per view, `rect` + `calculateProjection`.
|
|
@@ -1974,6 +1977,15 @@ export class PlayCanvasSplatViewer {
|
|
|
1974
1977
|
cam.camera.rect = new pc.Vec4(x / W, y / H, w / W, h / H);
|
|
1975
1978
|
placeNode(cam, e.pose);
|
|
1976
1979
|
}
|
|
1980
|
+
// handle.setLayerRig on the N-camera path: N display / post cameras mirroring these, the
|
|
1981
|
+
// views as located (this path draws the photo through them too — no remap here).
|
|
1982
|
+
if (this.layerRigs) {
|
|
1983
|
+
const rf = this._mapViews(entries, cache ? cache.rigAt : null);
|
|
1984
|
+
this.layerRigs.frame(entries, rect, null, rf.located ? rf.located.rig : cache ? cache.rig : null, {
|
|
1985
|
+
located: this.rigTrack && entries.length > 1 ? !!rf.located : null,
|
|
1986
|
+
residual: this.rigTrack?.lastResidual ?? null,
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1977
1989
|
}
|
|
1978
1990
|
this._updateFeather(entries[0].width * sx, entries[0].height * sy);
|
|
1979
1991
|
// handle.makeSbsMaterial: the eye split, scene-wide (a material only declares the uniform).
|
|
@@ -2949,7 +2961,6 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2949
2961
|
let removed = false;
|
|
2950
2962
|
// handle.setLayerRig: the per-rig cameras and where they read the declared rig from.
|
|
2951
2963
|
// Kill switch `nolayerrig`: requests are recorded but every layer stays on the eye camera.
|
|
2952
|
-
let warnedLayerRigPath = false;
|
|
2953
2964
|
function layerRigs() {
|
|
2954
2965
|
if (!viewer.layerRigs) {
|
|
2955
2966
|
viewer.layerRigs = new LayerRigCameras(viewer);
|
|
@@ -3044,26 +3055,31 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3044
3055
|
* on the camera rig ('camera'). No second full pass: one extra camera over the named layers,
|
|
3045
3056
|
* into the same target, in composition order. docs/playcanvas-adapter.md §setLayerRig and
|
|
3046
3057
|
* docs/proposals/layer-display-rig.md (the exact mapping, and why it is not Kooima).
|
|
3047
|
-
* `viewerDistance`
|
|
3058
|
+
* Options (`viewerDistance`, `gain`, `planeOffset`, `planeDistance`) are tile-wide and MERGE
|
|
3059
|
+
* (a key given replaces; `null` clears) — see setLayerRigOptions.
|
|
3060
|
+
* Works on both view paths (RenderViews and the N-camera fallback); layerRigState() says
|
|
3061
|
+
* whether it engaged and, if not, why.
|
|
3048
3062
|
*/
|
|
3049
3063
|
setLayerRig(layer, rig, o = {}) {
|
|
3050
3064
|
const r = validateLayerRig(layer, rig, o);
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3065
|
+
layerRigs().set(layer, r.rig, r.opts);
|
|
3066
|
+
return out;
|
|
3067
|
+
},
|
|
3068
|
+
/** Change the layer rig's tile-wide options live (no layer change): merge, `null` clears. */
|
|
3069
|
+
setLayerRigOptions(o = {}) {
|
|
3070
|
+
layerRigs().setOptions(validateLayerRigOptions(o, 'setLayerRigOptions'));
|
|
3056
3071
|
return out;
|
|
3057
3072
|
},
|
|
3058
|
-
/**
|
|
3073
|
+
/**
|
|
3074
|
+
* What setLayerRig is doing: `path` ('renderviews' | 'ncamera' | 'mono') and `engaged` (the
|
|
3075
|
+
* display-rig views were applied on the last drawn frame), `reason` when not, and the numbers
|
|
3076
|
+
* (`viewerDistance`, `gain`, `planeM` — the plane that lands on the glass, as a distance from
|
|
3077
|
+
* the photo's camera — `photoConvergenceM`, `planeOffset`, `located`).
|
|
3078
|
+
*/
|
|
3059
3079
|
layerRigState() {
|
|
3060
3080
|
const lr = viewer.layerRigs;
|
|
3061
|
-
return
|
|
3062
|
-
|
|
3063
|
-
disabled: !!lr?.disabled,
|
|
3064
|
-
rounded: !!lr?.last.rounded,
|
|
3065
|
-
gain: lr?.last.gain ?? null,
|
|
3066
|
-
};
|
|
3081
|
+
if (lr) return lr.state();
|
|
3082
|
+
return { display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'no layer on the display rig', viewerDistance: DEFAULT_VIEWER_DISTANCE_M, gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null };
|
|
3067
3083
|
},
|
|
3068
3084
|
/**
|
|
3069
3085
|
* An unlit material showing the left half of `texture` to left-eye views and the right half to
|
|
@@ -3248,8 +3264,8 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3248
3264
|
if (opts.displayRigLayers !== undefined) {
|
|
3249
3265
|
const d = opts.displayRigLayers;
|
|
3250
3266
|
const list = Array.isArray(d) ? d : d && Array.isArray(d.layers) ? d.layers : null;
|
|
3251
|
-
if (!list) throw new TypeError('@displayxr/inline3d/splat: displayRigLayers — expected an array of layers, or { layers, viewerDistance?, gain? }.');
|
|
3252
|
-
const o = Array.isArray(d) ? {} :
|
|
3267
|
+
if (!list) throw new TypeError('@displayxr/inline3d/splat: displayRigLayers — expected an array of layers, or { layers, viewerDistance?, gain?, planeOffset?, planeDistance? }.');
|
|
3268
|
+
const o = Array.isArray(d) ? {} : Object.fromEntries(Object.entries(d).filter(([k]) => k !== 'layers'));
|
|
3253
3269
|
for (const l of list) out.setLayerRig(l, 'display', o);
|
|
3254
3270
|
}
|
|
3255
3271
|
// Replay what the page did before this module arrived — exclude() above all, which a product
|
package/js/inline3d-splat.js
CHANGED
|
@@ -385,6 +385,9 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
385
385
|
setLayerRig() {
|
|
386
386
|
throw new Error("@displayxr/inline3d/splat: setLayerRig() is implemented on the PlayCanvas backend only (addSplat(…, { engine:'playcanvas' })).");
|
|
387
387
|
},
|
|
388
|
+
setLayerRigOptions() {
|
|
389
|
+
throw new Error("@displayxr/inline3d/splat: setLayerRigOptions() is implemented on the PlayCanvas backend only (addSplat(…, { engine:'playcanvas' })).");
|
|
390
|
+
},
|
|
388
391
|
/** Not on this backend: per-eye SBS materials are a PlayCanvas-backend feature. */
|
|
389
392
|
makeSbsMaterial() {
|
|
390
393
|
throw new Error("@displayxr/inline3d/splat: makeSbsMaterial() is implemented on the PlayCanvas backend only (addSplat(…, { engine:'playcanvas' })).");
|
|
@@ -773,7 +776,11 @@ function addSplatDeferred(wall, canvas, src, opts) {
|
|
|
773
776
|
pending.push(['setLayerRig', args]);
|
|
774
777
|
return out;
|
|
775
778
|
},
|
|
776
|
-
|
|
779
|
+
setLayerRigOptions: (...args) => {
|
|
780
|
+
pending.push(['setLayerRigOptions', args]);
|
|
781
|
+
return out;
|
|
782
|
+
},
|
|
783
|
+
layerRigState: () => ({ display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'the PlayCanvas adapter has not loaded yet', viewerDistance: 0.6, gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null }),
|
|
777
784
|
makeSbsMaterial: () => {
|
|
778
785
|
throw new Error('@displayxr/inline3d/splat: makeSbsMaterial() needs the engine — call it after `await handle.ready`.');
|
|
779
786
|
},
|
package/js/inline3d.js
CHANGED
|
@@ -130,6 +130,34 @@ const DISPLAY_MODE_METHODS = ['getDisplayInfo', 'getRenderingModes', 'requestRen
|
|
|
130
130
|
// mode or hardware-state change even while its tile's layer is closed (lazy mode), and one
|
|
131
131
|
// subscription covers every window in the document.
|
|
132
132
|
const DISPLAY_EVENTS = ['renderingmodechange', 'hardwaredisplaystatechange'];
|
|
133
|
+
// TRACKING STATE — is anyone in the display's 3D zone right now?
|
|
134
|
+
//
|
|
135
|
+
// `session.trackingState` is a DOMString ('tracking' | 'searching' | 'unknown', filled in on every
|
|
136
|
+
// frame) and `trackingstatechange` is a PLAIN Event with no payload, so the state is always READ
|
|
137
|
+
// off the session and never parsed out of the event. It comes from the DisplayXR runtime, which
|
|
138
|
+
// may hand the decision to the vendor plug-in (DisplayXR Browser patch 0195).
|
|
139
|
+
//
|
|
140
|
+
// 'searching' means the runtime's derived isTracking went false: nobody is in the display's
|
|
141
|
+
// supported 3D zone, or the display is in an untracked / 2D mode. It does NOT necessarily mean the
|
|
142
|
+
// tracker lost a face that is still there, and a page must not word its UI as if it did.
|
|
143
|
+
//
|
|
144
|
+
// An older browser has NEITHER the attribute NOR the event. That is not warning-worthy — it is
|
|
145
|
+
// every release up to and including 1.0.5 — so the SDK reports 'unknown', silently, and every
|
|
146
|
+
// consumer of this surface treats 'unknown' as "no opinion".
|
|
147
|
+
const TRACKING_EVENT = 'trackingstatechange';
|
|
148
|
+
const TRACKING_STATES = ['unknown', 'tracking', 'searching'];
|
|
149
|
+
// Everything on()/off() accept. `onDisplayModeChange` deliberately still fans out over
|
|
150
|
+
// DISPLAY_EVENTS alone: pages pass it one callback shaped for a mode event.
|
|
151
|
+
const MANAGER_EVENTS = [...DISPLAY_EVENTS, TRACKING_EVENT];
|
|
152
|
+
/** This session's tracking state, or null when the browser does not expose one at all. */
|
|
153
|
+
function readTrackingState(session) {
|
|
154
|
+
try {
|
|
155
|
+
const v = session ? session.trackingState : undefined;
|
|
156
|
+
return TRACKING_STATES.includes(v) ? v : null;
|
|
157
|
+
} catch {
|
|
158
|
+
return null; // an attribute that refuses to be read is not a capability
|
|
159
|
+
}
|
|
160
|
+
}
|
|
133
161
|
const hasDisplayModes = () => {
|
|
134
162
|
if (!hasLayer()) return false;
|
|
135
163
|
try {
|
|
@@ -514,7 +542,20 @@ function noteRigWinsOverHeight() {
|
|
|
514
542
|
* keeps the eye set coherent either way — and a mode change the page did NOT request
|
|
515
543
|
* (another tab, the shell, a panel opening flat) always snaps, because there is nothing
|
|
516
544
|
* to ramp from. Read the live state on `wall.modeSwitch`.
|
|
517
|
-
* @
|
|
545
|
+
* @param {'none'|'mono'} [opts.untrackedFallback='none'] What the SDK does to the image and video
|
|
546
|
+
* windows it owns the pixels of while NOBODY IS TRACKED (`wall.trackingState ===
|
|
547
|
+
* 'searching'`). WHOSE JOB THIS IS depends on the display's eye-tracking mode (runtime
|
|
548
|
+
* spec docs/specs/vendor/eye-tracking-modes.md). MANAGED — the default, and Leia's — the
|
|
549
|
+
* vendor owns tracking loss: it eases the eyes together and only reports 'searching' once
|
|
550
|
+
* the display is already 2D, so leave this at `'none'` (a flatten here would be a second
|
|
551
|
+
* transition). MANUAL — the vendor does nothing and the app handles it: set `'mono'`.
|
|
552
|
+
* `'none'` (default) changes nothing. `'mono'` eases each image/video window to its LEFT
|
|
553
|
+
* EYE IN BOTH HALVES of its side-by-side buffer on 'searching' (still a valid pair for the
|
|
554
|
+
* layer; no buffer reallocation) and back on 'tracking', over the mode switch's duration;
|
|
555
|
+
* 'unknown' leaves it where it is. Scene windows are never touched: the page owns those
|
|
556
|
+
* pixels — listen for `trackingstatechange` and do the same for a scene.
|
|
557
|
+
* @returns {Promise<Inline3D | {supported:false, trackingState:'unknown', error?:Error}>} the
|
|
558
|
+
* manager, which also carries
|
|
518
559
|
* the display API (`getDisplayInfo` / `getRenderingModes` / `requestRenderingMode` /
|
|
519
560
|
* `setStereoEnabled`, `on`/`off`) and `undock` — `{model, splat}` on a browser with
|
|
520
561
|
* `XRDisplayLayer.undock`, `null` on one without.
|
|
@@ -526,15 +567,18 @@ export async function createInline3D(opts = {}) {
|
|
|
526
567
|
rootMargin = '50% 0px',
|
|
527
568
|
autoChrome = true,
|
|
528
569
|
modeSwitch = null,
|
|
570
|
+
untrackedFallback = 'none',
|
|
529
571
|
} = opts;
|
|
530
|
-
|
|
572
|
+
// The unsupported shapes carry `trackingState` too, so a page can read it without branching on
|
|
573
|
+
// `supported` first. There is no on() here to pair with it: nothing would ever fire.
|
|
574
|
+
if (!inline3DAvailable()) return { supported: false, trackingState: 'unknown' };
|
|
531
575
|
let session;
|
|
532
576
|
try {
|
|
533
577
|
// requestSession is Blink-local and resolves immediately when the feature is present —
|
|
534
578
|
// the correct detection path (see inline3DAvailable's note on isSessionSupported).
|
|
535
579
|
session = await navigator.xr.requestSession('inline-3d');
|
|
536
580
|
} catch (e) {
|
|
537
|
-
return { supported: false, error: e };
|
|
581
|
+
return { supported: false, trackingState: 'unknown', error: e };
|
|
538
582
|
}
|
|
539
583
|
let refSpace = null;
|
|
540
584
|
try {
|
|
@@ -542,7 +586,7 @@ export async function createInline3D(opts = {}) {
|
|
|
542
586
|
} catch {
|
|
543
587
|
/* rAF still fires without a ref space; views are just null (fine for image/video). */
|
|
544
588
|
}
|
|
545
|
-
return new Inline3D(session, refSpace, { lazy, rootMargin, autoChrome, modeSwitch });
|
|
589
|
+
return new Inline3D(session, refSpace, { lazy, rootMargin, autoChrome, modeSwitch, untrackedFallback });
|
|
546
590
|
}
|
|
547
591
|
|
|
548
592
|
/**
|
|
@@ -594,7 +638,11 @@ function chromeTextPlates(root) {
|
|
|
594
638
|
}
|
|
595
639
|
|
|
596
640
|
class Inline3D {
|
|
597
|
-
constructor(
|
|
641
|
+
constructor(
|
|
642
|
+
session,
|
|
643
|
+
refSpace,
|
|
644
|
+
{ lazy, rootMargin, autoChrome = true, modeSwitch = null, untrackedFallback = 'none' }
|
|
645
|
+
) {
|
|
598
646
|
this.supported = true;
|
|
599
647
|
this.session = session;
|
|
600
648
|
this.refSpace = refSpace;
|
|
@@ -651,6 +699,25 @@ class Inline3D {
|
|
|
651
699
|
this._msArmedUp = false; // a ->3D request went out; the up-ramp waits for the panel to say 3D
|
|
652
700
|
this._msLastMs = null; // wall clock of the previous advance (null = the ramp has not ticked)
|
|
653
701
|
this._msTick = null; // the frames-stopped fallback timer; see _armModeSwitchTick
|
|
702
|
+
// ── tracking state (see TRACKING_EVENT) ───────────────────────────────────────────
|
|
703
|
+
// Read once here so `wall.trackingState` is honest before any event arrives. A page can open
|
|
704
|
+
// with nobody in front of the display, and that is a starting value, not a CHANGE, so nothing
|
|
705
|
+
// is emitted for it. A browser without the attribute reads null and stays 'unknown'.
|
|
706
|
+
this._trackingSupported = readTrackingState(session) !== null;
|
|
707
|
+
this._trackingState = this._trackingSupported ? readTrackingState(session) : 'unknown';
|
|
708
|
+
// What to do to image/video windows while nobody is tracked. 'none' is the default so every
|
|
709
|
+
// page that predates the option renders bit-identically (and MANAGED displays need nothing).
|
|
710
|
+
this._untrackedFallback = untrackedFallback === 'mono' ? 'mono' : 'none';
|
|
711
|
+
// ── untrackedFallback:'mono' — baked stereo eased to flat (see _trackBakedStereo) ───────
|
|
712
|
+
// An image/video tile's stereo is in its source pixels, so neither the rig collapse above nor
|
|
713
|
+
// the runtime's own easing of the views can reach it. 0..1 = how much of that baked disparity
|
|
714
|
+
// to show, eased toward the target the tracking state sets, over the mode switch's duration.
|
|
715
|
+
this._bakedStereo = 1;
|
|
716
|
+
this._bakedTarget = 1;
|
|
717
|
+
this._bakedAt = null; // frame time (ms) of the last ease step
|
|
718
|
+
this._bakedEaseS =
|
|
719
|
+
(Number.isFinite(msOpts.durationMs) ? Math.max(0, msOpts.durationMs) : MODE_SWITCH_DEFAULT_DURATION_MS) / 1000;
|
|
720
|
+
this._applyUntrackedTarget(); // a page that opens with nobody tracked starts heading flat
|
|
654
721
|
// Undock capabilities, refreshed off the first live layer (see _refreshUndock). Null is the
|
|
655
722
|
// load-bearing value: it means this browser has no XRDisplayLayer.undock at all.
|
|
656
723
|
this.undock = hasUndock() ? { model: false, splat: false } : null;
|
|
@@ -680,6 +747,7 @@ class Inline3D {
|
|
|
680
747
|
liveManager = this;
|
|
681
748
|
session.addEventListener('end', () => this._teardown());
|
|
682
749
|
this._bindDisplayEvents();
|
|
750
|
+
this._bindTrackingEvents();
|
|
683
751
|
this._scanChrome(); // page chrome usually exists before the session does
|
|
684
752
|
this._bindLifecycle();
|
|
685
753
|
this._armDprWatch();
|
|
@@ -901,6 +969,7 @@ class Inline3D {
|
|
|
901
969
|
* 3D per-pixel, so exclude()/unexclude() are stored-and-ignored (see _applyExclusion).
|
|
902
970
|
*/
|
|
903
971
|
_handle(canvas, win) {
|
|
972
|
+
const mgr = this; // for the getters below: an object-literal getter's `this` is the handle
|
|
904
973
|
return {
|
|
905
974
|
remove: () => this._remove(canvas),
|
|
906
975
|
exclude: (el) => {
|
|
@@ -940,6 +1009,10 @@ class Inline3D {
|
|
|
940
1009
|
* asked for. During the eased 2D<->3D transition the same applies with a FRACTION in place
|
|
941
1010
|
* of the 0: what leaves for the layer is your rig scaled by `wall.modeSwitch.factor`.
|
|
942
1011
|
*/
|
|
1012
|
+
/** The manager's `trackingState`, on the tile handle too ('tracking' | 'searching' | 'unknown'). */
|
|
1013
|
+
get trackingState() {
|
|
1014
|
+
return mgr._trackingState;
|
|
1015
|
+
},
|
|
943
1016
|
setViewRig: (rig) => {
|
|
944
1017
|
win.viewRig = rig || null;
|
|
945
1018
|
return this._pushViewRig(win);
|
|
@@ -1136,6 +1209,22 @@ class Inline3D {
|
|
|
1136
1209
|
* The hardware display state as last REPORTED by `hardwaredisplaystatechange` — `'2d'`,
|
|
1137
1210
|
* `'3d'`, or `null` when the browser has not said yet. Never what was last requested.
|
|
1138
1211
|
*/
|
|
1212
|
+
/**
|
|
1213
|
+
* Is anyone being tracked in front of this display right now?
|
|
1214
|
+
*
|
|
1215
|
+
* `'tracking'` a viewer is in the display's 3D zone.
|
|
1216
|
+
* `'searching'` the runtime's derived isTracking is FALSE — nobody is in the zone, or the
|
|
1217
|
+
* display is in an untracked / 2D mode. NOT "the tracker glitched".
|
|
1218
|
+
* `'unknown'` this browser has no tracking-state surface, or the session has ended.
|
|
1219
|
+
*
|
|
1220
|
+
* Mirrors `session.trackingState` as last read. Changes arrive as
|
|
1221
|
+
* `on('trackingstatechange', (state, ev) => …)`. Pages use it for "step back into view" hints;
|
|
1222
|
+
* `untrackedFallback: 'mono'` uses it to flatten image/video windows.
|
|
1223
|
+
*/
|
|
1224
|
+
get trackingState() {
|
|
1225
|
+
return this._trackingState;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1139
1228
|
get hardwareDisplayState() {
|
|
1140
1229
|
return this._hardwareDisplayState;
|
|
1141
1230
|
}
|
|
@@ -1711,13 +1800,20 @@ class Inline3D {
|
|
|
1711
1800
|
});
|
|
1712
1801
|
}
|
|
1713
1802
|
|
|
1714
|
-
/**
|
|
1715
|
-
|
|
1803
|
+
/**
|
|
1804
|
+
* Deliver one normalised event to every listener. A throwing page handler is contained.
|
|
1805
|
+
*
|
|
1806
|
+
* `args` overrides what the callback is CALLED with (the event object still selects the listener
|
|
1807
|
+
* set): `trackingstatechange` leads with the state string, because a page listening to it wants
|
|
1808
|
+
* the one value and not a wrapper.
|
|
1809
|
+
*/
|
|
1810
|
+
_emitDisplay(ev, args) {
|
|
1716
1811
|
const set = this._displayListeners.get(ev.type);
|
|
1717
1812
|
if (!set) return;
|
|
1813
|
+
const argv = args || [ev];
|
|
1718
1814
|
for (const cb of [...set]) {
|
|
1719
1815
|
try {
|
|
1720
|
-
cb(
|
|
1816
|
+
cb(...argv);
|
|
1721
1817
|
} catch (err) {
|
|
1722
1818
|
console.error(`[inline3d] ${ev.type} listener threw`, err);
|
|
1723
1819
|
}
|
|
@@ -1725,14 +1821,18 @@ class Inline3D {
|
|
|
1725
1821
|
}
|
|
1726
1822
|
|
|
1727
1823
|
/**
|
|
1728
|
-
* Listen for `renderingmodechange` / `hardwaredisplaystatechange`
|
|
1729
|
-
* handle's `on()` doc for the payload shapes
|
|
1824
|
+
* Listen for `renderingmodechange` / `hardwaredisplaystatechange` / `trackingstatechange` on
|
|
1825
|
+
* this manager. See the handle's `on()` doc for the payload shapes — the tracking callback is
|
|
1826
|
+
* handed the STATE STRING first: `(state, { type, state })`. Returns an unsubscribe function.
|
|
1827
|
+
*
|
|
1828
|
+
* Subscribing to `trackingstatechange` on a browser with no tracking state is legal and silent:
|
|
1829
|
+
* the listener is kept, nothing fires it, and `trackingState` stays 'unknown'.
|
|
1730
1830
|
*/
|
|
1731
1831
|
on(type, cb) {
|
|
1732
1832
|
if (typeof cb !== 'function') throw new TypeError('[inline3d] on() takes (type, function).');
|
|
1733
|
-
if (!
|
|
1833
|
+
if (!MANAGER_EVENTS.includes(type)) {
|
|
1734
1834
|
throw new TypeError(
|
|
1735
|
-
`[inline3d] on() knows ${
|
|
1835
|
+
`[inline3d] on() knows ${MANAGER_EVENTS.join(' / ')}, got ${JSON.stringify(type)}.`
|
|
1736
1836
|
);
|
|
1737
1837
|
}
|
|
1738
1838
|
let set = this._displayListeners.get(type);
|
|
@@ -2380,6 +2480,17 @@ class Inline3D {
|
|
|
2380
2480
|
return;
|
|
2381
2481
|
}
|
|
2382
2482
|
const halfDst = c.width / 2;
|
|
2483
|
+
// How much of the source's baked disparity to show: the eased 2D<->3D ramp (the mode) and
|
|
2484
|
+
// the runtime's tracking state (_trackBakedStereo), whichever is flatter. Below 1 the RIGHT half becomes
|
|
2485
|
+
// the left eye with the right eye blended over it, so 0 is the left eye in both halves —
|
|
2486
|
+
// flat, and still a valid SBS pair for the live layer (a 1:1 mono buffer here would be woven
|
|
2487
|
+
// as a squeezed pair). See _trackBakedStereo.
|
|
2488
|
+
const stereo = Math.min(this._stereoFactor, this._bakedStereo);
|
|
2489
|
+
if (stereo < 1) {
|
|
2490
|
+
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, win.cornerRadius, win.feather); // L
|
|
2491
|
+
drawEyeBlend(ctx, src, srcW / 2, srcH, halfDst, c.height, stereo, win.cornerRadius, win.feather); // L+R
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2383
2494
|
// A single stretched draw maps SBS source → SBS buffer (left→left, right→right); the
|
|
2384
2495
|
// per-eye path is only needed to bake decoration (rounded corners / edge feather), which
|
|
2385
2496
|
// MUST be applied to each eye separately — see drawEye/featherEye.
|
|
@@ -2391,6 +2502,68 @@ class Inline3D {
|
|
|
2391
2502
|
}
|
|
2392
2503
|
}
|
|
2393
2504
|
|
|
2505
|
+
/**
|
|
2506
|
+
* Subscribe to `trackingstatechange`, once per manager, and only where the ATTRIBUTE exists: the
|
|
2507
|
+
* event is payload-free, so a browser that fired it without `session.trackingState` would leave
|
|
2508
|
+
* nothing to read. Independent of `_bindDisplayEvents` — a browser may ship them in either order.
|
|
2509
|
+
*/
|
|
2510
|
+
_bindTrackingEvents() {
|
|
2511
|
+
const session = this.session;
|
|
2512
|
+
if (!session || typeof session.addEventListener !== 'function') return;
|
|
2513
|
+
if (!this._trackingSupported) return;
|
|
2514
|
+
session.addEventListener(TRACKING_EVENT, () => {
|
|
2515
|
+
this._setTrackingState(readTrackingState(session) || 'unknown'); // READ, never parse
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
/**
|
|
2520
|
+
* Adopt a new tracking state: act on it first (the fallback target, so a listener sees the state
|
|
2521
|
+
* it is told about already in effect), then tell the page. On CHANGE only — a repeated state is
|
|
2522
|
+
* not an event, and the value read at construction is a starting value, not a change.
|
|
2523
|
+
*/
|
|
2524
|
+
_setTrackingState(state) {
|
|
2525
|
+
const next = TRACKING_STATES.includes(state) ? state : 'unknown';
|
|
2526
|
+
if (next === this._trackingState) return;
|
|
2527
|
+
this._trackingState = next;
|
|
2528
|
+
this._applyUntrackedTarget();
|
|
2529
|
+
const ev = { type: TRACKING_EVENT, state: next };
|
|
2530
|
+
this._emitDisplay(ev, [next, ev]);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* Where baked stereo is heading under `untrackedFallback`. `'none'`: always the full pair.
|
|
2535
|
+
* `'mono'`: `'searching'` heads to flat, `'tracking'` back; `'unknown'` (no display yet, the
|
|
2536
|
+
* first frame, a session that ended) leaves it where it is — never read as flat.
|
|
2537
|
+
*/
|
|
2538
|
+
_applyUntrackedTarget() {
|
|
2539
|
+
if (this._untrackedFallback !== 'mono') {
|
|
2540
|
+
this._bakedTarget = 1;
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (this._trackingState === 'searching') this._bakedTarget = 0;
|
|
2544
|
+
else if (this._trackingState === 'tracking') this._bakedTarget = 1;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* One frame of the baked-stereo ease (untrackedFallback:'mono'): toward the target the tracking
|
|
2549
|
+
* state set, at a rate that
|
|
2550
|
+
* crosses 0..1 in the mode switch's duration, so an untracked flatten looks like a page-requested
|
|
2551
|
+
* 2D switch. Scene tiles are not involved — they render from the views, which the runtime
|
|
2552
|
+
* eases itself.
|
|
2553
|
+
*/
|
|
2554
|
+
_trackBakedStereo(t) {
|
|
2555
|
+
// `trackingState` is filled in on every frame; reading it here as well as on the event keeps
|
|
2556
|
+
// the state right even if an event was coalesced away. Emits only on a real change.
|
|
2557
|
+
if (this._trackingSupported) this._setTrackingState(readTrackingState(this.session) || 'unknown');
|
|
2558
|
+
const dt = this._bakedAt === null || !Number.isFinite(t) ? 0 : Math.max(0, (t - this._bakedAt) / 1000);
|
|
2559
|
+
this._bakedAt = Number.isFinite(t) ? t : this._bakedAt;
|
|
2560
|
+
const cur = this._bakedStereo;
|
|
2561
|
+
const tgt = this._bakedTarget;
|
|
2562
|
+
if (cur === tgt) return;
|
|
2563
|
+
const step = this._bakedEaseS > 0 ? dt / this._bakedEaseS : 1;
|
|
2564
|
+
this._bakedStereo = tgt > cur ? Math.min(tgt, cur + step) : Math.max(tgt, cur - step);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2394
2567
|
/**
|
|
2395
2568
|
* Re-commit the pixels the canvas already holds, unchanged — the cheapest "last decoded frame"
|
|
2396
2569
|
* there is, because the last decoded frame is already in the backing store.
|
|
@@ -2447,6 +2620,7 @@ class Inline3D {
|
|
|
2447
2620
|
this._advanceModeSwitch();
|
|
2448
2621
|
const pose = this.refSpace ? f.getViewerPose(this.refSpace) : null;
|
|
2449
2622
|
const views = pose ? pose.views : null;
|
|
2623
|
+
this._trackBakedStereo(t);
|
|
2450
2624
|
for (const win of this._windows.values()) {
|
|
2451
2625
|
if (!win.layer) continue;
|
|
2452
2626
|
if (win.kind === 'scene') {
|
|
@@ -2669,6 +2843,10 @@ class Inline3D {
|
|
|
2669
2843
|
this._settleFirstWoven(win, false, 'session-ended');
|
|
2670
2844
|
}
|
|
2671
2845
|
this._windows.clear();
|
|
2846
|
+
// Nobody is tracked through a session that has ended: say so ONCE, before the listeners are
|
|
2847
|
+
// dropped, so a page that gated its UI on 'tracking' is released rather than left latched. A
|
|
2848
|
+
// browser that never reported one is already 'unknown' and this is silent.
|
|
2849
|
+
this._setTrackingState('unknown');
|
|
2672
2850
|
// Page listeners go with the session that fed them: a manager whose session has ended will
|
|
2673
2851
|
// never emit again, and holding the callbacks would keep the page's closures alive.
|
|
2674
2852
|
this._displayListeners.clear();
|
|
@@ -2711,6 +2889,24 @@ function drawEye(ctx, src, sx, sy, sw, sh, dx, dy, dw, dh, radius, feather) {
|
|
|
2711
2889
|
}
|
|
2712
2890
|
}
|
|
2713
2891
|
|
|
2892
|
+
// The right half of a partly flattened SBS paint: the LEFT eye, with the right eye over it at
|
|
2893
|
+
// `amount` (0 = left only). Clipped and feathered once, as one eye, for the same reason drawEye is.
|
|
2894
|
+
function drawEyeBlend(ctx, src, halfSrc, srcH, dx, dh, amount, radius, feather) {
|
|
2895
|
+
ctx.save();
|
|
2896
|
+
if (radius > 0 && ctx.roundRect) {
|
|
2897
|
+
ctx.beginPath();
|
|
2898
|
+
ctx.roundRect(dx, 0, dx, dh, radius);
|
|
2899
|
+
ctx.clip();
|
|
2900
|
+
}
|
|
2901
|
+
ctx.drawImage(src, 0, 0, halfSrc, srcH, dx, 0, dx, dh);
|
|
2902
|
+
if (amount > 0) {
|
|
2903
|
+
ctx.globalAlpha = amount;
|
|
2904
|
+
ctx.drawImage(src, halfSrc, 0, halfSrc, srcH, dx, 0, dx, dh);
|
|
2905
|
+
}
|
|
2906
|
+
ctx.restore();
|
|
2907
|
+
if (feather > 0) featherEye(ctx, dx, 0, dx, dh, feather);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2714
2910
|
// Fade this EYE's outer edges to transparent, so the 3D window dissolves into the page
|
|
2715
2911
|
// instead of ending at a hard rectangle. Same spirit as the runtime feathering a 3D zone's
|
|
2716
2912
|
// edge — but note that is the hardware WISH MASK (it drives the hardware display state, never
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a glasses-free-3D window on a DisplayXR display, inside an ordinary web page. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"import": "./js/inline3d-model.js"
|
|
33
33
|
},
|
|
34
34
|
"./package.json": "./package.json",
|
|
35
|
+
"./player": {
|
|
36
|
+
"types": "./player.d.ts",
|
|
37
|
+
"import": "./js/inline3d-player.js"
|
|
38
|
+
},
|
|
35
39
|
"./undock": {
|
|
36
40
|
"types": "./index.d.ts",
|
|
37
41
|
"import": "./js/inline3d-undock.js"
|
|
@@ -61,11 +65,13 @@
|
|
|
61
65
|
"js/inline3d-model-playcanvas.js",
|
|
62
66
|
"js/inline3d-pc-look.js",
|
|
63
67
|
"js/inline3d-playcanvas-engine.js",
|
|
68
|
+
"js/inline3d-player.js",
|
|
64
69
|
"index.d.ts",
|
|
65
70
|
"three.d.ts",
|
|
66
71
|
"viewer.d.ts",
|
|
67
72
|
"splat.d.ts",
|
|
68
73
|
"model.d.ts",
|
|
74
|
+
"player.d.ts",
|
|
69
75
|
"CHANGELOG.md",
|
|
70
76
|
"README.md",
|
|
71
77
|
"LICENSE",
|