@displayxr/inline3d 1.29.0 → 1.31.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.
@@ -380,6 +380,7 @@ export function addModel(wall, canvas, src, opts = {}) {
380
380
  renderScale,
381
381
  feather,
382
382
  logTag: '[inline3d/model]',
383
+ viewerEase: opts.viewerEase,
383
384
  }).useEyeCamera(EyeCamera, EdgeFeather);
384
385
 
385
386
  if (envMap) viewer.scene.environment = envMap;
@@ -0,0 +1,153 @@
1
+ // inline3d-splat-deferred.js — the engine:'playcanvas' handle, shared by both splat entries.
2
+ //
3
+ // Internal. ./splat (Spark by default) and ./splat/playcanvas (PlayCanvas only) both build their
4
+ // PlayCanvas handle here. This module must never import `three` or `@sparkjsdev/spark`, directly
5
+ // or through anything it imports: that is what lets ./splat/playcanvas keep both off the page
6
+ // (test/splat-pc-entry.test.mjs walks the import graph to hold it to that).
7
+
8
+ import { normalizeCameraPose } from './inline3d-splat-shared.js';
9
+ import { validateEffectCall } from './inline3d-splat-effects.js';
10
+
11
+ /**
12
+ * The `engine: 'playcanvas'` handle, returned SYNCHRONOUSLY like the Spark one.
13
+ *
14
+ * The adapter module is loaded on demand, so for a moment the handle exists before its
15
+ * implementation does. Rather than make every caller await something new, the handle starts as
16
+ * stubs that QUEUE: `exclude()` (which a product page calls on the very next line), `setPose`,
17
+ * `setFocus`, `remove` — and the adapter replays the queue into the real implementation, on the
18
+ * SAME object, the moment it arrives. `ready` resolves to this object, as it does on Spark.
19
+ * Fields (`viewer`, `mesh`, `rig`, …) are null until then; `viewer` in particular appears one
20
+ * module-load later than on Spark.
21
+ */
22
+ /** setRig's type/options shape, checked at the call before the adapter has loaded (it re-checks all of it). */
23
+ function validateSetRigArgs(type, o) {
24
+ if (!['display', 'camera', 'auto'].includes(type)) {
25
+ throw new Error(`@displayxr/inline3d/splat: setRig("${type}") — expected 'display', 'camera' or 'auto'.`);
26
+ }
27
+ if (o !== undefined && (o === null || typeof o !== 'object')) {
28
+ throw new TypeError('@displayxr/inline3d/splat: setRig options must be an object.');
29
+ }
30
+ }
31
+
32
+ export function addSplatDeferred(wall, canvas, src, opts) {
33
+ const pending = [];
34
+ const queue = (name) => (...args) => {
35
+ pending.push([name, args]);
36
+ return name === 'setFocus' ? out : undefined;
37
+ };
38
+ const page = opts.controls === 'page';
39
+ const pageOnly = (name) => () => {
40
+ throw new Error(
41
+ `@displayxr/inline3d/splat: ${name}() is not available with controls:'page' — the page owns the ` +
42
+ 'camera. Drive it with handle.setCameraPose(matrixWorld, { verticalFovDeg, near, far }).',
43
+ );
44
+ };
45
+ // Before the adapter lands, a page calling setCameraPose every frame keeps ONE pending pose
46
+ // (validated now, so a bad call throws at its own line): last call wins, as it will after.
47
+ let pendingPose = null;
48
+ const out = {
49
+ backend: 'playcanvas',
50
+ engine: null,
51
+ viewer: null,
52
+ mesh: null,
53
+ frame: null,
54
+ camera: null,
55
+ rig: null,
56
+ perf: null,
57
+ setPose: page ? pageOnly('setPose') : queue('setPose'),
58
+ resetPose: page ? pageOnly('resetPose') : queue('resetPose'),
59
+ setCameraPose(matrixWorld, o) {
60
+ if (!page) {
61
+ throw new Error(
62
+ "@displayxr/inline3d/splat: setCameraPose() needs addSplat(…, { controls:'page' }) — " +
63
+ 'with the default controls the SDK owns the camera (use setPose).',
64
+ );
65
+ }
66
+ const pose = normalizeCameraPose(matrixWorld, o);
67
+ if (pendingPose) pendingPose[1] = [pose.matrixWorld, pose];
68
+ else pending.push((pendingPose = ['setCameraPose', [pose.matrixWorld, pose]]));
69
+ return out;
70
+ },
71
+ getCameraPose: () =>
72
+ pendingPose
73
+ ? {
74
+ matrixWorld: Float32Array.from(pendingPose[1][1].matrixWorld),
75
+ verticalFovDeg: pendingPose[1][1].verticalFovDeg,
76
+ near: pendingPose[1][1].near,
77
+ far: pendingPose[1][1].far,
78
+ convergence: pendingPose[1][1].convergence,
79
+ }
80
+ : null,
81
+ setFocus: queue('setFocus'),
82
+ // A swap requested before the first asset has landed runs once it has (the adapter's own
83
+ // setSource replaces this stub on the same object by then).
84
+ setSource: (...args) => out.ready.then(() => out.setSource(...args)),
85
+ prepareSource: (...args) => out.ready.then(() => out.prepareSource(...args)),
86
+ // A rig switch before the first asset has landed: validated now (controls:'page' and a bad
87
+ // type throw at the call's own line), applied once it has.
88
+ setRig: page
89
+ ? pageOnly('setRig')
90
+ : (type, o) => {
91
+ validateSetRigArgs(type, o);
92
+ return out.ready.then(() => out.setRig(type, o));
93
+ },
94
+ // A video before the first asset has landed: controls:'page' throws at the call's own line,
95
+ // the rest is validated by the adapter's setVideo, which runs once the asset is on screen.
96
+ setVideo: page ? pageOnly('setVideo') : (src, o) => out.ready.then(() => out.setVideo(src, o)),
97
+ // Effects before the adapter has loaded: validated NOW (a bad call throws at its own line),
98
+ // then run once the first asset is on screen.
99
+ playEffect: (name, o) => {
100
+ validateEffectCall(name, o, 'play');
101
+ return out.ready.then(() => out.playEffect(name, o));
102
+ },
103
+ setEffect: (name, params) => {
104
+ validateEffectCall(name, params, 'set');
105
+ pending.push(['setEffect', [name, params]]);
106
+ return out;
107
+ },
108
+ stopEffect: queue('stopEffect'),
109
+ // A layer-rig request before the adapter has loaded: queued, replayed in order once it has
110
+ // (it needs no asset, only the engine's layers — which the adapter resolves lazily).
111
+ setLayerRig: (...args) => {
112
+ pending.push(['setLayerRig', args]);
113
+ return out;
114
+ },
115
+ setLayerRigOptions: (...args) => {
116
+ pending.push(['setLayerRigOptions', args]);
117
+ return out;
118
+ },
119
+ 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 }),
120
+ makeSbsMaterial: () => {
121
+ throw new Error('@displayxr/inline3d/splat: makeSbsMaterial() needs the engine — call it after `await handle.ready`.');
122
+ },
123
+ effects: () => [],
124
+ getFocus: () => null,
125
+ // A plain data slot the adapter reads at call time, so a callback assigned on the very next
126
+ // line after addSplat — before the module has loaded — is the one that fires.
127
+ onFocusChange: null,
128
+ pick: () => null,
129
+ // Splat accounting (resident / budget / first frame); null until the adapter has loaded.
130
+ stats: () => null,
131
+ remove: queue('remove'),
132
+ exclude: queue('exclude'),
133
+ unexclude: queue('unexclude'),
134
+ };
135
+ // `firstWoven` exists from the first line, like every other field a page reads right away; the
136
+ // adapter settles it with the core handle's own once the module has loaded.
137
+ out.firstWoven = new Promise((resolve) => {
138
+ out._resolveFirstWoven = resolve;
139
+ });
140
+ // The ONE owner of `ready`: the adapter returns its load promise and never touches this field.
141
+ out.ready = import('./inline3d-splat-playcanvas.js')
142
+ .then((m) => m.attachPlayCanvasSplat(out, wall, canvas, src, opts, pending))
143
+ .catch((err) => {
144
+ // The adapter warns about its own load failures; this is for the module not arriving.
145
+ if (!out.viewer) console.warn('[inline3d/splat] engine:playcanvas failed to start', err);
146
+ if (out._resolveFirstWoven) {
147
+ out._resolveFirstWoven(Object.freeze({ woven: false, confirmed: false, reason: 'layer-failed', ms: 0 }));
148
+ delete out._resolveFirstWoven;
149
+ }
150
+ throw err;
151
+ });
152
+ return out;
153
+ }
@@ -0,0 +1,45 @@
1
+ // inline3d-splat-pc-entry.js — `@displayxr/inline3d/splat/playcanvas`: addSplat on PlayCanvas only.
2
+ //
3
+ // EXPERIMENTAL, like ./splat. Same addSplat, same handle, same options, with the engine fixed to
4
+ // 'playcanvas'. The difference is what the page downloads: ./splat statically imports `three`
5
+ // and `@sparkjsdev/spark` because Spark is its default engine and its Spark path is synchronous,
6
+ // so a page passing engine:'playcanvas' there still pays for both (~1.7 MB gzipped). This entry
7
+ // imports neither, directly or transitively. `playcanvas` itself is still loaded on demand, when
8
+ // the first splat is added.
9
+ //
10
+ // import { createInline3D } from '@displayxr/inline3d';
11
+ // import { addSplat } from '@displayxr/inline3d/splat/playcanvas';
12
+ //
13
+ // const wall = await createInline3D();
14
+ // const scene = addSplat(wall, canvas, 'lake.sog', { reveal: 'sweep' });
15
+ //
16
+ // Not here: measureSplatBounds (it measures a Spark mesh — import it from ./splat).
17
+
18
+ import { resolveRevealOption } from './inline3d-splat-effects.js';
19
+ import { CAPTURE_FITS, playcanvasCannotRead, resolveControls } from './inline3d-splat-shared.js';
20
+ import { addSplatDeferred } from './inline3d-splat-deferred.js';
21
+
22
+ export { applySplatPerf, SPLAT_PERF_PRESETS } from './inline3d-splat-perf.js';
23
+ export { readSogCamera, readSogMeta } from './inline3d-sog.js';
24
+ export { resolveRig } from './inline3d-splat-rig.js';
25
+
26
+ /** addSplat with `engine: 'playcanvas'`; the same validation, at the call, as ./splat's. */
27
+ export function addSplat(wall, canvas, src, opts = {}) {
28
+ if (opts.engine !== undefined && opts.engine !== 'playcanvas') {
29
+ throw new Error(
30
+ `@displayxr/inline3d/splat/playcanvas: engine "${opts.engine}" — this entry is PlayCanvas ` +
31
+ "only. Import addSplat from '@displayxr/inline3d/splat' for Spark.",
32
+ );
33
+ }
34
+ if (opts.captureFit !== undefined && !CAPTURE_FITS.includes(opts.captureFit)) {
35
+ throw new Error(
36
+ `@displayxr/inline3d/splat: captureFit "${opts.captureFit}" — expected ` +
37
+ `${CAPTURE_FITS.map((f) => `'${f}'`).join(' or ')}.`,
38
+ );
39
+ }
40
+ resolveControls(opts);
41
+ resolveRevealOption(opts.reveal);
42
+ const why = playcanvasCannotRead(src, opts);
43
+ if (why) throw new Error(`@displayxr/inline3d/splat: ${why}`);
44
+ return addSplatDeferred(wall, canvas, src, { ...opts, engine: 'playcanvas' });
45
+ }
@@ -67,6 +67,7 @@ import {
67
67
  sequenceSpans,
68
68
  } from './inline3d-splat-effects.js';
69
69
  import { LiveOutgoing, resolveOutgoingOption, defaultOutgoing, yieldIdle } from './inline3d-splat-live.js';
70
+ import { viewerEaseFor, frameTrackingState } from './inline3d-viewer-ease.js';
70
71
  import { VideoPlane, validateSetVideo, PAGE_VIDEO_ERROR, eyeSplit, EYE_SPLIT_UNIFORM, makeSbsMaterial } from './inline3d-splat-video.js';
71
72
  import { LayerRigCameras, validateLayerRig, validateLayerRigOptions, DEFAULT_VIEWER_DISTANCE_M } from './inline3d-splat-layer-rig.js';
72
73
  import { RigTracker, remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
@@ -817,8 +818,13 @@ export class PlayCanvasSplatViewer {
817
818
  sky = false,
818
819
  pageCamera = false,
819
820
  toneMapping = 'none',
821
+ viewerEase,
820
822
  } = opts;
821
823
  this.canvas = canvas;
824
+ // The tracking-acquisition ease (./inline3d-viewer-ease.js): undefined = the session's
825
+ // createInline3D({ viewerEase }) default. Built on the first 3D frame, which knows the session.
826
+ this._viewerEaseOpt = viewerEase;
827
+ this.viewerEase = null;
822
828
  // controls:'page': the PAGE owns the camera (setPageCamera). No orbit, no idle, no fit; the
823
829
  // rig node carries the page's matrix instead of the inverse pivot.
824
830
  this.pageCamera = pageCamera === true;
@@ -1194,7 +1200,7 @@ export class PlayCanvasSplatViewer {
1194
1200
  }
1195
1201
 
1196
1202
  /** wall.addScene's frame callback. Validate BEFORE drawing; replay the last good frame else. */
1197
- onFrame(views, layer) {
1203
+ onFrame(views, layer, frame) {
1198
1204
  if (this._disposed) return;
1199
1205
  if (this._mode !== '3d') this.stopMono();
1200
1206
  // The rig these views were located with: Blink chained the rig declared BEFORE this callback,
@@ -1224,6 +1230,9 @@ export class PlayCanvasSplatViewer {
1224
1230
  vps.push(vp);
1225
1231
  }
1226
1232
  this._cacheGood(views, vps);
1233
+ // Ease a tracking acquisition/loss on the copies (never the XRViews), before anything —
1234
+ // the rig map, the layer rig, the live outgoing photo — reads them.
1235
+ (this.viewerEase ||= viewerEaseFor(frame, this._viewerEaseOpt)).apply(this._lastGood.entries, frameTrackingState(frame));
1227
1236
  this._drawEntries(this._lastGood.entries, this._lastGood);
1228
1237
  }
1229
1238
 
@@ -2139,6 +2148,7 @@ export class PlayCanvasSplatViewer {
2139
2148
  if (this._disposed) return;
2140
2149
  const box = this.canvas.getBoundingClientRect();
2141
2150
  if (box.width < 1 || box.height < 1) return;
2151
+ this.viewerEase?.reset(); // a new window size moves every projection: not a viewer jump
2142
2152
  const dpr = Math.min((typeof window !== 'undefined' && window.devicePixelRatio) || 1, 2) * this.renderScale;
2143
2153
  // Clamp to the device's GL limits BEFORE sizing (./inline3d-buffer-limit.js). Measured on an
2144
2154
  // Adreno 740 tablet: MAX_TEXTURE_SIZE 4096, a 5120-wide request silently became a 4096-wide
@@ -3072,6 +3082,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
3072
3082
  nearClip: opts.nearClip,
3073
3083
  farClip: opts.farClip,
3074
3084
  sky: opts.sky,
3085
+ viewerEase: opts.viewerEase,
3075
3086
  });
3076
3087
 
3077
3088
  let handle = null;
@@ -3394,9 +3405,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
3394
3405
  let initialViewRig = null; // the rig addScene is created with (controls:'page'), else the shorthand
3395
3406
  if (wall && wall.supported) {
3396
3407
  const onFrame = diag
3397
- ? (views, layer) => {
3408
+ ? (views, layer, frame) => {
3398
3409
  const t = performance.now();
3399
- viewer.onFrame(views, layer);
3410
+ viewer.onFrame(views, layer, frame);
3400
3411
  diag.frame(views, t); // after the draw: the overlay state is what this frame showed
3401
3412
  }
3402
3413
  : viewer.onFrame;
@@ -23,8 +23,9 @@ import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';
23
23
  import { EyeCamera, EdgeFeather, cameraRigFromCamera } from './inline3d-three.js';
24
24
  import { SceneViewer, boundsFromPositions } from './inline3d-viewer.js';
25
25
  import { readSogCamera } from './inline3d-sog.js';
26
- import { resolveRevealOption, effectsNotOnSpark, validateEffectCall } from './inline3d-splat-effects.js';
26
+ import { resolveRevealOption, effectsNotOnSpark } from './inline3d-splat-effects.js';
27
27
  import { applySplatPerf, splatPerfMeshOptions } from './inline3d-splat-perf.js';
28
+ import { addSplatDeferred } from './inline3d-splat-deferred.js';
28
29
  import {
29
30
  toArray3,
30
31
  canvasNdc,
@@ -36,7 +37,6 @@ import {
36
37
  isStreamedUrl,
37
38
  STREAMED_NEEDS_PLAYCANVAS,
38
39
  resolveControls,
39
- normalizeCameraPose,
40
40
  declareViewRig,
41
41
  } from './inline3d-splat-shared.js';
42
42
  import {
@@ -246,6 +246,7 @@ export function addSplat(wall, canvas, src, opts = {}) {
246
246
  renderScale,
247
247
  feather,
248
248
  logTag: '[inline3d/splat]',
249
+ viewerEase: opts.viewerEase,
249
250
  }).useEyeCamera(EyeCamera, EdgeFeather);
250
251
 
251
252
  // Spark renders through the ordinary three.js pipeline, so splats and meshes co-exist and
@@ -677,149 +678,6 @@ export function addSplat(wall, canvas, src, opts = {}) {
677
678
  return out;
678
679
  }
679
680
 
680
- /**
681
- * The `engine: 'playcanvas'` handle, returned SYNCHRONOUSLY like the Spark one.
682
- *
683
- * The adapter module is loaded on demand, so for a moment the handle exists before its
684
- * implementation does. Rather than make every caller await something new, the handle starts as
685
- * stubs that QUEUE: `exclude()` (which a product page calls on the very next line), `setPose`,
686
- * `setFocus`, `remove` — and the adapter replays the queue into the real implementation, on the
687
- * SAME object, the moment it arrives. `ready` resolves to this object, as it does on Spark.
688
- * Fields (`viewer`, `mesh`, `rig`, …) are null until then; `viewer` in particular appears one
689
- * module-load later than on Spark.
690
- */
691
- /** setRig's type/options shape, checked at the call before the adapter has loaded (it re-checks all of it). */
692
- function validateSetRigArgs(type, o) {
693
- if (!['display', 'camera', 'auto'].includes(type)) {
694
- throw new Error(`@displayxr/inline3d/splat: setRig("${type}") — expected 'display', 'camera' or 'auto'.`);
695
- }
696
- if (o !== undefined && (o === null || typeof o !== 'object')) {
697
- throw new TypeError('@displayxr/inline3d/splat: setRig options must be an object.');
698
- }
699
- }
700
-
701
- function addSplatDeferred(wall, canvas, src, opts) {
702
- const pending = [];
703
- const queue = (name) => (...args) => {
704
- pending.push([name, args]);
705
- return name === 'setFocus' ? out : undefined;
706
- };
707
- const page = opts.controls === 'page';
708
- const pageOnly = (name) => () => {
709
- throw new Error(
710
- `@displayxr/inline3d/splat: ${name}() is not available with controls:'page' — the page owns the ` +
711
- 'camera. Drive it with handle.setCameraPose(matrixWorld, { verticalFovDeg, near, far }).',
712
- );
713
- };
714
- // Before the adapter lands, a page calling setCameraPose every frame keeps ONE pending pose
715
- // (validated now, so a bad call throws at its own line): last call wins, as it will after.
716
- let pendingPose = null;
717
- const out = {
718
- backend: 'playcanvas',
719
- engine: null,
720
- viewer: null,
721
- mesh: null,
722
- frame: null,
723
- camera: null,
724
- rig: null,
725
- perf: null,
726
- setPose: page ? pageOnly('setPose') : queue('setPose'),
727
- resetPose: page ? pageOnly('resetPose') : queue('resetPose'),
728
- setCameraPose(matrixWorld, o) {
729
- if (!page) {
730
- throw new Error(
731
- "@displayxr/inline3d/splat: setCameraPose() needs addSplat(…, { controls:'page' }) — " +
732
- 'with the default controls the SDK owns the camera (use setPose).',
733
- );
734
- }
735
- const pose = normalizeCameraPose(matrixWorld, o);
736
- if (pendingPose) pendingPose[1] = [pose.matrixWorld, pose];
737
- else pending.push((pendingPose = ['setCameraPose', [pose.matrixWorld, pose]]));
738
- return out;
739
- },
740
- getCameraPose: () =>
741
- pendingPose
742
- ? {
743
- matrixWorld: Float32Array.from(pendingPose[1][1].matrixWorld),
744
- verticalFovDeg: pendingPose[1][1].verticalFovDeg,
745
- near: pendingPose[1][1].near,
746
- far: pendingPose[1][1].far,
747
- convergence: pendingPose[1][1].convergence,
748
- }
749
- : null,
750
- setFocus: queue('setFocus'),
751
- // A swap requested before the first asset has landed runs once it has (the adapter's own
752
- // setSource replaces this stub on the same object by then).
753
- setSource: (...args) => out.ready.then(() => out.setSource(...args)),
754
- prepareSource: (...args) => out.ready.then(() => out.prepareSource(...args)),
755
- // A rig switch before the first asset has landed: validated now (controls:'page' and a bad
756
- // type throw at the call's own line), applied once it has.
757
- setRig: page
758
- ? pageOnly('setRig')
759
- : (type, o) => {
760
- validateSetRigArgs(type, o);
761
- return out.ready.then(() => out.setRig(type, o));
762
- },
763
- // A video before the first asset has landed: controls:'page' throws at the call's own line,
764
- // the rest is validated by the adapter's setVideo, which runs once the asset is on screen.
765
- setVideo: page ? pageOnly('setVideo') : (src, o) => out.ready.then(() => out.setVideo(src, o)),
766
- // Effects before the adapter has loaded: validated NOW (a bad call throws at its own line),
767
- // then run once the first asset is on screen.
768
- playEffect: (name, o) => {
769
- validateEffectCall(name, o, 'play');
770
- return out.ready.then(() => out.playEffect(name, o));
771
- },
772
- setEffect: (name, params) => {
773
- validateEffectCall(name, params, 'set');
774
- pending.push(['setEffect', [name, params]]);
775
- return out;
776
- },
777
- stopEffect: queue('stopEffect'),
778
- // A layer-rig request before the adapter has loaded: queued, replayed in order once it has
779
- // (it needs no asset, only the engine's layers — which the adapter resolves lazily).
780
- setLayerRig: (...args) => {
781
- pending.push(['setLayerRig', args]);
782
- return out;
783
- },
784
- setLayerRigOptions: (...args) => {
785
- pending.push(['setLayerRigOptions', args]);
786
- return out;
787
- },
788
- 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 }),
789
- makeSbsMaterial: () => {
790
- throw new Error('@displayxr/inline3d/splat: makeSbsMaterial() needs the engine — call it after `await handle.ready`.');
791
- },
792
- effects: () => [],
793
- getFocus: () => null,
794
- // A plain data slot the adapter reads at call time, so a callback assigned on the very next
795
- // line after addSplat — before the module has loaded — is the one that fires.
796
- onFocusChange: null,
797
- pick: () => null,
798
- // Splat accounting (resident / budget / first frame); null until the adapter has loaded.
799
- stats: () => null,
800
- remove: queue('remove'),
801
- exclude: queue('exclude'),
802
- unexclude: queue('unexclude'),
803
- };
804
- // `firstWoven` exists from the first line, like every other field a page reads right away; the
805
- // adapter settles it with the core handle's own once the module has loaded.
806
- out.firstWoven = new Promise((resolve) => {
807
- out._resolveFirstWoven = resolve;
808
- });
809
- // The ONE owner of `ready`: the adapter returns its load promise and never touches this field.
810
- out.ready = import('./inline3d-splat-playcanvas.js')
811
- .then((m) => m.attachPlayCanvasSplat(out, wall, canvas, src, opts, pending))
812
- .catch((err) => {
813
- // The adapter warns about its own load failures; this is for the module not arriving.
814
- if (!out.viewer) console.warn('[inline3d/splat] engine:playcanvas failed to start', err);
815
- if (out._resolveFirstWoven) {
816
- out._resolveFirstWoven(Object.freeze({ woven: false, confirmed: false, reason: 'layer-failed', ms: 0 }));
817
- delete out._resolveFirstWoven;
818
- }
819
- throw err;
820
- });
821
- return out;
822
- }
823
681
 
824
682
  /**
825
683
  * Percentile bounds from the loaded splats — the fallback when no sidecar was supplied.