@displayxr/inline3d 1.33.1 → 1.35.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 +32 -0
- package/camera.d.ts +23 -0
- package/dist/call.js +4 -4
- package/js/inline3d-call.js +25 -2
- package/js/inline3d-camera.js +105 -8
- package/js/inline3d-model-entry.js +4 -1
- package/js/inline3d-model-playcanvas.js +2 -0
- package/js/inline3d-splat-playcanvas.js +23 -1
- package/model.d.ts +7 -0
- package/package.json +1 -1
package/js/inline3d-call.js
CHANGED
|
@@ -1127,6 +1127,9 @@ class Call {
|
|
|
1127
1127
|
hello: this.hello(),
|
|
1128
1128
|
transport: this.transport,
|
|
1129
1129
|
peers: [...this.tiles.values()].map((tile) => tile.diag()),
|
|
1130
|
+
// The self view's ACTUAL weave state (web#131): route, woven, the fallback reason and the
|
|
1131
|
+
// tile's firstWoven result — null without a self view.
|
|
1132
|
+
self: this.self ? this.self.diag() : null,
|
|
1130
1133
|
mono3D: { ...this._mono3DInfo(), lift: this.liftApi, pool: this.liftPool ? this.liftPool.list() : [] },
|
|
1131
1134
|
speaker: this.speakerId,
|
|
1132
1135
|
depth: this.depth,
|
|
@@ -1866,14 +1869,34 @@ class SelfTile {
|
|
|
1866
1869
|
else {
|
|
1867
1870
|
this.detach();
|
|
1868
1871
|
this.cam = cam;
|
|
1869
|
-
|
|
1872
|
+
// onRouteChange: the view drops to its flat left eye by itself when the wall says the tile
|
|
1873
|
+
// will not weave (firstWoven → woven:false, web#131), and the badge must follow it.
|
|
1874
|
+
this.view = addCameraView(wall, this.canvas, cam, {
|
|
1875
|
+
mirror: true,
|
|
1876
|
+
autoConverge: false,
|
|
1877
|
+
aspect: this.call.o.tileAspect,
|
|
1878
|
+
onRouteChange: (route, st) => {
|
|
1879
|
+
if (this.cam !== cam) return; // a view this tile already replaced
|
|
1880
|
+
this.call.log('self-route', { route, woven: st.woven, reason: st.reason });
|
|
1881
|
+
this._badge();
|
|
1882
|
+
},
|
|
1883
|
+
});
|
|
1870
1884
|
}
|
|
1871
1885
|
this._badge();
|
|
1872
1886
|
}
|
|
1873
1887
|
|
|
1888
|
+
/** 3D only while the view is on the woven route — a layer the wall failed has already taken it flat. */
|
|
1874
1889
|
_badge() {
|
|
1875
1890
|
const t = this.call.t;
|
|
1876
|
-
|
|
1891
|
+
const woven = !!(this.view && this.view.woven);
|
|
1892
|
+
this.badge.replaceChildren(el('b', { text: `${t('you')} · ${woven ? t('badge3D') : t('badge2D')}` }));
|
|
1893
|
+
this.badge.dataset.route = this.route || '';
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/** For `call.diagnostics().self`: what the self view actually shows. */
|
|
1897
|
+
diag() {
|
|
1898
|
+
if (!this.view) return { route: null, woven: false, reason: 'no-camera', firstWoven: null, layerRetries: 0 };
|
|
1899
|
+
return this.view.weaveState();
|
|
1877
1900
|
}
|
|
1878
1901
|
|
|
1879
1902
|
onWallLost() {
|
package/js/inline3d-camera.js
CHANGED
|
@@ -51,6 +51,11 @@ export const CAMERA_SDK = 'inline3d-camera/1';
|
|
|
51
51
|
const AUTO_CONV_INTERVAL_MS = 200;
|
|
52
52
|
const AUTO_CONV_EYE_WIDTH = 240;
|
|
53
53
|
const FRAME_WAIT_MS = 4000;
|
|
54
|
+
// A self view whose layer the wall could not build (`firstWoven` → `woven:false, 'layer-failed'`)
|
|
55
|
+
// goes flat and re-registers a few times with backoff — the remote tile's rule (web#131).
|
|
56
|
+
const LAYER_RETRIES = 4;
|
|
57
|
+
const LAYER_RETRY_BASE_MS = 1500;
|
|
58
|
+
const LAYER_RETRY_MAX_MS = 20000;
|
|
54
59
|
const RECORD_MIME_CANDIDATES = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm', 'video/mp4'];
|
|
55
60
|
|
|
56
61
|
const hasDoc = () => typeof document !== 'undefined' && document && typeof document.createElement === 'function';
|
|
@@ -471,7 +476,12 @@ function sizeFlat(canvas) {
|
|
|
471
476
|
* @param {object|null} wall a `createInline3D()` / `sharedInline3D()` result (may be unsupported) or null
|
|
472
477
|
* @param {HTMLCanvasElement} canvas the tile — its CSS box is the shape the viewer sees
|
|
473
478
|
* @param {StereoCamera} cam
|
|
474
|
-
*
|
|
479
|
+
* On a 3D wall the view follows its tile's `firstWoven`: a window the wall will not weave
|
|
480
|
+
* (`'layer-failed'`, `'session-ended'`) drops the view to the flat left eye — never the packed
|
|
481
|
+
* pair — with `fallbackReason` saying why, and `onRouteChange(route, state)` tells the page (a
|
|
482
|
+
* badge reads `route`). A refused layer is retried a few times; `_reroute(true, wall)` recovers.
|
|
483
|
+
*
|
|
484
|
+
* @param {{ mirror?: boolean, autoConverge?: boolean, depth?: number, aspect?: number, onRouteChange?: Function }} [opts]
|
|
475
485
|
*/
|
|
476
486
|
export function addCameraView(wall, canvas, cam, opts = {}) {
|
|
477
487
|
if (!canvas || typeof canvas.getContext !== 'function') throw new TypeError(`${TAG} addCameraView(wall, canvas, cam, opts) needs a canvas`);
|
|
@@ -494,6 +504,15 @@ class CameraView {
|
|
|
494
504
|
this.setDepth(o.depth);
|
|
495
505
|
this.route = null;
|
|
496
506
|
this.handle = null;
|
|
507
|
+
// Why a pair on a 3D-capable wall is shown FLAT (web#131): the wall said this tile's window
|
|
508
|
+
// will not weave (`firstWoven` → woven:false — 'layer-failed', 'session-ended'). Null while
|
|
509
|
+
// nothing has failed. A packed pair is never the visible fallback: the view paints one eye.
|
|
510
|
+
this.fallbackReason = null;
|
|
511
|
+
// The current registration's settled `firstWoven`, null while pending / not registered.
|
|
512
|
+
this._fw = null;
|
|
513
|
+
this._onRouteChange = typeof o.onRouteChange === 'function' ? o.onRouteChange : null;
|
|
514
|
+
this._layerFails = 0;
|
|
515
|
+
this._layerTimer = 0;
|
|
497
516
|
this.buffer = null; // the SBS canvas the wall repaints the tile from (woven route)
|
|
498
517
|
this.removed = false;
|
|
499
518
|
this._raf = 0;
|
|
@@ -507,11 +526,30 @@ class CameraView {
|
|
|
507
526
|
if (this._loop) this._start();
|
|
508
527
|
}
|
|
509
528
|
|
|
510
|
-
/**
|
|
529
|
+
/**
|
|
530
|
+
* True while the view is on the woven route: registered on a 3D wall, and the wall has not said
|
|
531
|
+
* the tile will not weave (a failed layer drops the view to `'flat-left'`). Route is
|
|
532
|
+
* `'woven-sbs'` (3D on the panel) | `'flat-left'` (a pair, shown flat) | `'flat'` (mono).
|
|
533
|
+
*/
|
|
511
534
|
get woven() {
|
|
512
535
|
return this.route === 'woven-sbs';
|
|
513
536
|
}
|
|
514
537
|
|
|
538
|
+
/**
|
|
539
|
+
* What is actually on the panel, for diagnostics: `{ route, woven, reason, firstWoven,
|
|
540
|
+
* layerRetries }`. `firstWoven` is the current registration's settled result, `'pending'`
|
|
541
|
+
* before it settles, null off the woven route.
|
|
542
|
+
*/
|
|
543
|
+
weaveState() {
|
|
544
|
+
return {
|
|
545
|
+
route: this.route,
|
|
546
|
+
woven: this.woven,
|
|
547
|
+
reason: this.fallbackReason,
|
|
548
|
+
firstWoven: this.route === 'woven-sbs' ? (this._fw ? { ...this._fw } : 'pending') : null,
|
|
549
|
+
layerRetries: this._layerFails,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
515
553
|
/** The per-eye convergence shift currently painted, source px. */
|
|
516
554
|
get convergencePx() {
|
|
517
555
|
return this.conv.current;
|
|
@@ -543,20 +581,77 @@ class CameraView {
|
|
|
543
581
|
return this.autoConverge;
|
|
544
582
|
}
|
|
545
583
|
|
|
546
|
-
/**
|
|
584
|
+
/**
|
|
585
|
+
* Re-register on the wall (the call does this when the weave goes live, #172, or a session is
|
|
586
|
+
* recovered). A FORCED reroute also clears a layer fallback (web#131): a wall that came back
|
|
587
|
+
* gets a fresh registration. An unforced one keeps the view flat while a fallback stands.
|
|
588
|
+
*/
|
|
547
589
|
_reroute(force, wall) {
|
|
548
590
|
if (wall !== undefined) this.wall = wall;
|
|
549
591
|
if (this.removed) return;
|
|
592
|
+
if (force && this.fallbackReason) {
|
|
593
|
+
this.fallbackReason = null;
|
|
594
|
+
clearTimeout(this._layerTimer);
|
|
595
|
+
this._layerTimer = 0;
|
|
596
|
+
}
|
|
550
597
|
const cam = this.cam;
|
|
551
598
|
const live = cam.state === 'live' && cam.video;
|
|
552
|
-
const
|
|
599
|
+
const wovenOk = cam.format === 'sbs' && live && this.wall && this.wall.supported && !this.fallbackReason;
|
|
600
|
+
const route = wovenOk ? 'woven-sbs' : cam.format === 'sbs' ? 'flat-left' : 'flat';
|
|
553
601
|
if (!force && route === this.route) return;
|
|
602
|
+
const prev = this.route;
|
|
554
603
|
this._unregister();
|
|
555
604
|
this.route = route;
|
|
556
|
-
if (route === 'woven-sbs')
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
605
|
+
if (route === 'woven-sbs') this._registerWoven();
|
|
606
|
+
if (route !== prev) this._routeChanged();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
_registerWoven() {
|
|
610
|
+
if (!this.buffer) this.buffer = document.createElement('canvas');
|
|
611
|
+
this._paintWoven(); // the first frame exists before the layer does
|
|
612
|
+
const handle = this.wall.addImage(this.canvas, this.buffer);
|
|
613
|
+
this.handle = handle;
|
|
614
|
+
this._fw = null;
|
|
615
|
+
const fw = handle && handle.firstWoven;
|
|
616
|
+
if (!fw || typeof fw.then !== 'function') return;
|
|
617
|
+
fw.then((res) => {
|
|
618
|
+
// A result for a registration we already left (removed, re-registered) says nothing now.
|
|
619
|
+
if (this.handle !== handle || this.removed || !res) return;
|
|
620
|
+
this._fw = { woven: !!res.woven, confirmed: !!res.confirmed, reason: res.reason || null, ms: res.ms };
|
|
621
|
+
this.cam.log?.('view-first-woven', { woven: !!res.woven, reason: res.reason || null });
|
|
622
|
+
if (res.woven) {
|
|
623
|
+
this._layerFails = 0;
|
|
624
|
+
this._routeChanged(); // the state settled; the route did not move
|
|
625
|
+
} else if (res.reason !== 'removed') this._layerLost(res.reason || 'layer-failed');
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The wall said this window will not weave (web#131): the canvas must never be left showing the
|
|
631
|
+
* packed pair under a 3D badge. Go flat (one eye, mirrored) and say why. A layer the browser
|
|
632
|
+
* refused is retried a few times with backoff while the wall stays up (the remote tile's rule);
|
|
633
|
+
* a session that ended waits for `_reroute(true, wall)` with the new wall.
|
|
634
|
+
*/
|
|
635
|
+
_layerLost(reason) {
|
|
636
|
+
this.fallbackReason = reason;
|
|
637
|
+
this._reroute(false);
|
|
638
|
+
if (reason !== 'layer-failed' || this._layerFails >= LAYER_RETRIES) return;
|
|
639
|
+
const delay = Math.min(LAYER_RETRY_MAX_MS, LAYER_RETRY_BASE_MS * Math.pow(2, this._layerFails++));
|
|
640
|
+
this.cam.log?.('view-layer-retry', { attempt: this._layerFails, inMs: delay });
|
|
641
|
+
clearTimeout(this._layerTimer);
|
|
642
|
+
this._layerTimer = setTimeout(() => {
|
|
643
|
+
this._layerTimer = 0;
|
|
644
|
+
if (this.removed || this.fallbackReason !== reason) return;
|
|
645
|
+
this._reroute(true);
|
|
646
|
+
}, delay);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
_routeChanged() {
|
|
650
|
+
if (!this._onRouteChange) return;
|
|
651
|
+
try {
|
|
652
|
+
this._onRouteChange(this.route, this.weaveState());
|
|
653
|
+
} catch (err) {
|
|
654
|
+
console.error(`${TAG} onRouteChange threw`, err);
|
|
560
655
|
}
|
|
561
656
|
}
|
|
562
657
|
|
|
@@ -672,6 +767,8 @@ class CameraView {
|
|
|
672
767
|
this.removed = true;
|
|
673
768
|
if (this._raf) cancelAnimationFrame(this._raf);
|
|
674
769
|
this._raf = 0;
|
|
770
|
+
clearTimeout(this._layerTimer);
|
|
771
|
+
this._layerTimer = 0;
|
|
675
772
|
this._unregister();
|
|
676
773
|
this.cam._views?.delete(this);
|
|
677
774
|
}
|
|
@@ -54,7 +54,7 @@ export function resolveModelEngine(opts = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Options that exist only on the PlayCanvas backend: an error on an explicit engine:'three'. */
|
|
57
|
-
const PLAYCANVAS_ONLY = ['playcanvas', 'environmentRotation', 'controls', 'comfortDepth', 'onBeforeFrame', 'antialias', 'preserveDrawingBuffer', 'nearClip', 'farClip', 'orbitMaxDeg', 'orbitEase', 'zoom'];
|
|
57
|
+
const PLAYCANVAS_ONLY = ['playcanvas', 'environmentRotation', 'controls', 'comfortDepth', 'onBeforeFrame', 'antialias', 'preserveDrawingBuffer', 'nearClip', 'farClip', 'orbitMaxDeg', 'orbitEase', 'orbitStyle', 'zoom'];
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Everything that can be checked before a module loads. Throws at CALL time — a page bug is true
|
|
@@ -67,6 +67,9 @@ export function validateModelCall(canvas, src, opts, route) {
|
|
|
67
67
|
if (typeof src !== 'string' || !src) {
|
|
68
68
|
throw new Error('@displayxr/inline3d/model: addModel(wall, canvas, src) — `src` must be the URL of a .glb or .gltf.');
|
|
69
69
|
}
|
|
70
|
+
if (opts.orbitStyle !== undefined && opts.orbitStyle !== 'turntable' && opts.orbitStyle !== 'tilt') {
|
|
71
|
+
throw new Error(`@displayxr/inline3d/model: orbitStyle must be 'turntable' or 'tilt' (got ${String(opts.orbitStyle)}).`);
|
|
72
|
+
}
|
|
70
73
|
if (route.engine === 'three' && route.explicit) {
|
|
71
74
|
const bad = PLAYCANVAS_ONLY.filter((k) => opts[k] !== undefined && !(k === 'controls' && opts[k] === 'viewer'));
|
|
72
75
|
if (bad.length) {
|
|
@@ -459,6 +459,8 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
459
459
|
flipY: false, // glTF is already Y-up
|
|
460
460
|
orbitMaxDeg: opts.orbitMaxDeg,
|
|
461
461
|
orbitEase: opts.orbitEase,
|
|
462
|
+
// A mesh turns all the way round, like the three path's SceneViewer: not the photo tilt.
|
|
463
|
+
orbitStyle: opts.orbitStyle ?? 'turntable',
|
|
462
464
|
zoom: opts.zoom,
|
|
463
465
|
feather,
|
|
464
466
|
nearClip: opts.nearClip,
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
ORBIT_MAX_DEG,
|
|
99
99
|
ORBIT_TAU_DRAG_S,
|
|
100
100
|
ORBIT_TAU_REST_S,
|
|
101
|
+
DRAG_DEG_PER_TILE,
|
|
101
102
|
WHEEL_LINE_PX,
|
|
102
103
|
WHEEL_PAGE_PX,
|
|
103
104
|
WHEEL_MAX_PX,
|
|
@@ -812,6 +813,7 @@ export class PlayCanvasSplatViewer {
|
|
|
812
813
|
pitchLimit = PITCH_LIMIT,
|
|
813
814
|
orbitMaxDeg = ORBIT_MAX_DEG,
|
|
814
815
|
orbitEase = {},
|
|
816
|
+
orbitStyle = 'tilt',
|
|
815
817
|
zoom,
|
|
816
818
|
feather = 0,
|
|
817
819
|
captureFit = 'height',
|
|
@@ -853,6 +855,10 @@ export class PlayCanvasSplatViewer {
|
|
|
853
855
|
// from where the press started, easing with τ = orbitEase.drag; release relaxes back with
|
|
854
856
|
// τ = orbitEase.rest. `_orbitMode` is 'drag' | 'rest' | null (null = ordinary damping).
|
|
855
857
|
this.orbitMaxDeg = orbitMaxDeg;
|
|
858
|
+
// 'tilt' (the default; photo splats: a capped tilt that relaxes) or 'turntable' (./model: an
|
|
859
|
+
// object you turn all the way round — SceneViewer's cumulative drag, DRAG_DEG_PER_TILE per
|
|
860
|
+
// tile width, no relax).
|
|
861
|
+
this.orbitStyle = orbitStyle === 'turntable' ? 'turntable' : 'tilt';
|
|
856
862
|
this.orbitEase = { drag: orbitEase.drag ?? ORBIT_TAU_DRAG_S, rest: orbitEase.rest ?? ORBIT_TAU_REST_S };
|
|
857
863
|
this._orbitMode = null;
|
|
858
864
|
// Zoom bounds + relax (./inline3d-splat-shared.js §ZOOM). Wheel and pinch clamp to
|
|
@@ -2414,6 +2420,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2414
2420
|
};
|
|
2415
2421
|
const endDrag = () => {
|
|
2416
2422
|
dragging = false;
|
|
2423
|
+
if (this.orbitStyle === 'turntable') return; // stays where it was turned
|
|
2417
2424
|
this._targetYaw = restYaw;
|
|
2418
2425
|
this._targetPitch = clamp(restPitch, this.pitchLimit[0], this.pitchLimit[1]);
|
|
2419
2426
|
this._orbitMode = 'rest';
|
|
@@ -2443,7 +2450,8 @@ export class PlayCanvasSplatViewer {
|
|
|
2443
2450
|
// Rest = where the pose was heading when pressed (an idle turntable's yaw included).
|
|
2444
2451
|
restYaw = this._orbitMode ? restYaw : this._targetYaw;
|
|
2445
2452
|
restPitch = this._orbitMode ? restPitch : this._targetPitch;
|
|
2446
|
-
|
|
2453
|
+
// turntable: the ordinary damping (no drag ease, no relax), as SceneViewer
|
|
2454
|
+
this._orbitMode = this.orbitStyle === 'turntable' ? null : 'drag';
|
|
2447
2455
|
this._lastInput = now();
|
|
2448
2456
|
};
|
|
2449
2457
|
this._onMove = (ev) => {
|
|
@@ -2457,6 +2465,20 @@ export class PlayCanvasSplatViewer {
|
|
|
2457
2465
|
}
|
|
2458
2466
|
if (!dragging) return;
|
|
2459
2467
|
const box = el.getBoundingClientRect();
|
|
2468
|
+
if (this.orbitStyle === 'turntable') {
|
|
2469
|
+
// SceneViewer's drag (./inline3d-viewer.js): a full drag across the tile is a half turn,
|
|
2470
|
+
// accumulated from the last move, near face following the pointer on both axes.
|
|
2471
|
+
this._targetYaw += ((ev.clientX - startX) / Math.max(box.width, 1)) * DRAG_DEG_PER_TILE;
|
|
2472
|
+
this._targetPitch = clamp(
|
|
2473
|
+
this._targetPitch + ((ev.clientY - startY) / Math.max(box.height, 1)) * DRAG_DEG_PER_TILE,
|
|
2474
|
+
this.pitchLimit[0],
|
|
2475
|
+
this.pitchLimit[1],
|
|
2476
|
+
);
|
|
2477
|
+
startX = ev.clientX;
|
|
2478
|
+
startY = ev.clientY;
|
|
2479
|
+
this._lastInput = now();
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2460
2482
|
const dx = (ev.clientX - startX) / Math.max(box.width, 1);
|
|
2461
2483
|
const dy = (ev.clientY - startY) / Math.max(box.height, 1);
|
|
2462
2484
|
const max = this.orbitMaxDeg;
|
package/model.d.ts
CHANGED
|
@@ -126,6 +126,13 @@ export interface ModelOptions {
|
|
|
126
126
|
farClip?: number;
|
|
127
127
|
/** PlayCanvas only: the drag's tilt cap in degrees (default 15, tilt-and-relax). */
|
|
128
128
|
orbitMaxDeg?: number;
|
|
129
|
+
/**
|
|
130
|
+
* PlayCanvas only: how a drag turns the model. `'turntable'` (the default for addModel, as on the
|
|
131
|
+
* three path): cumulative, a full drag across the tile is a half turn, and it stays where you
|
|
132
|
+
* leave it. `'tilt'` (addSplat's mapping for photo splats): capped at ±`orbitMaxDeg` from the
|
|
133
|
+
* press and relaxing back to rest on release.
|
|
134
|
+
*/
|
|
135
|
+
orbitStyle?: 'turntable' | 'tilt';
|
|
129
136
|
/** PlayCanvas only: the orbit's easing time constants, seconds. */
|
|
130
137
|
orbitEase?: { drag?: number; rest?: number };
|
|
131
138
|
/** PlayCanvas only: zoom bounds + relax (wheel, pinch, setPose) — see the splat option. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|