@displayxr/inline3d 1.7.1 → 1.9.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/splat.d.ts CHANGED
@@ -34,6 +34,52 @@ export interface SplatPerfOptions {
34
34
  lodSplatCount?: number;
35
35
  /** Minimum on-screen splat size multiplier (needs `lod`); up to ~5 is often invisible. */
36
36
  lodRenderScale?: number;
37
+ /**
38
+ * `engine: 'playcanvas'` only — engine-native knobs, passed straight to `app.scene.gsplat`
39
+ * (and winning over the Spark-knob mapping).
40
+ *
41
+ * `splatBudget` is a splat count **per tile, all views included**: every view of a tile is
42
+ * drawn through one engine camera, so one budget covers both eyes of a 3D tile. It only acts
43
+ * on a Streamed SOG (a flat `.sog` draws every splat). Unset on a Streamed SOG = 600k
44
+ * (`STREAMED_SPLAT_BUDGET`); unset on anything else, or `perf: false` = the engine's 1M.
45
+ */
46
+ splatBudget?: number;
47
+ /**
48
+ * `engine: 'playcanvas'`, Streamed SOG only: how a chunk's LOD is chosen. Unset = the engine's
49
+ * `'distance'`.
50
+ */
51
+ lodMode?: 'distance' | 'error';
52
+ /** Streamed SOG only: camera travel (in the file's own units) before LOD re-evaluates. Engine default 1. */
53
+ lodUpdateDistance?: number;
54
+ /** Streamed SOG only: camera rotation in degrees before LOD re-evaluates. Engine default 0 (off). */
55
+ lodUpdateAngle?: number;
56
+ /** Streamed SOG only: how many coarser levels may stand in while a finer one streams. Engine default 0. */
57
+ lodUnderfillLimit?: number;
58
+ /** `engine: 'playcanvas'` only: cull splats whose quad DIAMETER is under this many px. */
59
+ minPixelSize?: number;
60
+ /** `engine: 'playcanvas'` only: the forward-pass alpha floor (engine default 1/255). */
61
+ alphaClipForward?: number;
62
+ /** `engine: 'playcanvas'` only: the engine's AA compensation, for AA-trained assets. */
63
+ antiAlias?: boolean;
64
+ }
65
+
66
+ /**
67
+ * The PlayCanvas backend's viewer (`engine: 'playcanvas'`): the SceneViewer pose surface without
68
+ * three. Not field-compatible with SceneViewer — see docs/playcanvas-adapter.md.
69
+ */
70
+ export interface PlayCanvasSplatViewer {
71
+ idleSpin: number;
72
+ readonly is3D: boolean;
73
+ depthOffset: number;
74
+ /** The engine's `AppBase`, once booted. */
75
+ readonly app: unknown;
76
+ fitTo(center: number[], extent: number[]): void;
77
+ setPose(pose?: OrbitPose): void;
78
+ getPose(opts?: { target?: boolean }): Required<OrbitPose>;
79
+ resetPose(): void;
80
+ getSubjectBounds(): SubjectBounds & { front: number; back: number; scale: number };
81
+ setFocus(point: number[] | { x: number; y: number; z: number } | null, opts?: { snap?: boolean; recentre?: boolean }): PlayCanvasSplatViewer;
82
+ getFocus(opts?: { target?: boolean }): { x: number; y: number; z: number };
37
83
  }
38
84
 
39
85
  /** Camera intrinsics for ONE eye, in pixels, OpenCV convention. */
@@ -92,14 +138,26 @@ export interface ResolvedRig {
92
138
  focalEqMm: number;
93
139
  /** The live focus, in the splat's own space. */
94
140
  focus: number[];
141
+ /**
142
+ * Which step answered the focus. The order: `caller` › `caller-convergence` › `block` (a
143
+ * considered block focus) › `nearest-clump` (the nearest substantial disparity clump in the
144
+ * central half of the frame — needs the block's or the caller's lens) › `block-cloud-median` (a
145
+ * block focus a converter computed as a whole-cloud median) › `median-disparity` › `default`.
146
+ */
95
147
  focusSource:
96
148
  | 'caller'
97
149
  | 'caller-convergence'
98
150
  | 'block'
151
+ | 'nearest-clump'
152
+ | 'block-cloud-median'
99
153
  | 'median-disparity'
100
154
  | 'default'
101
155
  | 'picked'
102
156
  | 'set';
157
+ /** The block's own `focus.source` string (e.g. `'convergence'`, `'cloud-median'`), for diagnostics. */
158
+ blockFocusSource: string | null;
159
+ /** Fraction of the central crop's opacity-weighted mass the winning clump carried (`nearest-clump` only). */
160
+ clumpMassFrac: number | null;
103
161
  /** What Space returns to. */
104
162
  focusDefault: number[];
105
163
  focusDefaultSource: string;
@@ -112,6 +170,27 @@ export interface ResolvedRig {
112
170
  }
113
171
 
114
172
  export interface SplatOptions {
173
+ /**
174
+ * Which renderer. `'spark'` (the default) is three.js + Spark. `'playcanvas'` is the PlayCanvas
175
+ * engine (optional peer `playcanvas >=2.22.3 <3`, loaded by dynamic import only when asked):
176
+ * same handle, reads `.sog` / `.ply` / a Streamed-SOG `lod-meta.json`. Anything else throws.
177
+ *
178
+ * A Streamed SOG is loaded BY URL only — its `lod-meta.json`, or the directory holding it (a
179
+ * URL ending in `/`). It is a directory of chunk files named by relative path, so bytes of a
180
+ * `lod-meta.json` throw at call time with a message giving the URL form, and so does a streamed
181
+ * URL with `engine: 'spark'`.
182
+ */
183
+ engine?: 'spark' | 'playcanvas';
184
+ /**
185
+ * `engine: 'playcanvas'` only: the WebGL context's `preserveDrawingBuffer` (default false) —
186
+ * the knob for the weave's zero-copy read race on large canvases.
187
+ */
188
+ preserveDrawingBuffer?: boolean;
189
+ /**
190
+ * `engine: 'playcanvas'` only: the `playcanvas` module namespace to use instead of
191
+ * `import('playcanvas')` — for a page that already bundles its own copy.
192
+ */
193
+ playcanvas?: unknown;
115
194
  /** Metres of world the tile's height spans (default 0.24). */
116
195
  virtualDisplayHeight?: number;
117
196
  /**
@@ -124,7 +203,17 @@ export interface SplatOptions {
124
203
  flipY?: boolean;
125
204
  /** Degrees/second of turntable once idle (default 8). */
126
205
  idleSpin?: number;
206
+ /**
207
+ * Drag + wheel. On the PlayCanvas backend the drag is TILT-AND-RELAX: measured as a fraction of
208
+ * the canvas box from the press, it tilts up to ±`orbitMaxDeg` (a half-width swipe reaches it)
209
+ * and relaxes back to rest on release; `idleSpin` resumes once at rest. On Spark (SceneViewer)
210
+ * it is still the cumulative turntable (a full-width drag = 180°).
211
+ */
127
212
  orbit?: boolean;
213
+ /** PlayCanvas: the largest drag tilt, degrees, either axis (default 15). */
214
+ orbitMaxDeg?: number;
215
+ /** PlayCanvas: easing time constants, seconds — `drag` while held (0.2), `rest` after release (0.6). */
216
+ orbitEase?: { drag?: number; rest?: number };
128
217
  fit?: 'contain' | 'height' | 'cover' | 'none';
129
218
  /** Fraction of the tile the subject may occupy (default 0.8) — width AND height. */
130
219
  margin?: number;
@@ -138,7 +227,11 @@ export interface SplatOptions {
138
227
  /** Per-eye buffer scale; 0.5–0.7 is usually free (default 1). */
139
228
  renderScale?: number;
140
229
  feather?: number;
141
- /** Minimum ms between splat sorts. Defaults to 16 so both eyes share one sort per frame. */
230
+ /**
231
+ * Spark: minimum ms between splat sorts (default 16, so both eyes share one sort per frame).
232
+ * PlayCanvas: accepted and has no effect — the engine re-sorts when the camera ROTATES, with one
233
+ * directional sort serving every view.
234
+ */
142
235
  sortIntervalMs?: number;
143
236
  /**
144
237
  * Cut overdraw. UNSET changes nothing — every Spark default stays where Spark put it, so an
@@ -150,7 +243,7 @@ export interface SplatOptions {
150
243
  * `true`, −5…−20 % measured) and `'aggressive'` (−22 %) tighten the quad extent instead, which
151
244
  * is the axis that actually pays on the web; both move pixels.
152
245
  */
153
- perf?: true | 'exact' | 'balanced' | 'aggressive' | SplatPerfOptions;
246
+ perf?: boolean | 'exact' | 'balanced' | 'aggressive' | SplatPerfOptions;
154
247
  /**
155
248
  * Which view rig. `'auto'` (the default) reads it off the ASSET — a `.sog` carrying a `camera`
156
249
  * block was lifted from a photograph and gets a camera rig that conserves the recording
@@ -158,6 +251,14 @@ export interface SplatOptions {
158
251
  * detectable when `src` is BYTES.
159
252
  */
160
253
  rig?: 'auto' | 'display' | 'camera';
254
+ /**
255
+ * Camera rig only: what gives when the canvas is not the capture's shape. `'height'` (default,
256
+ * the 1.7 behaviour) keeps the capture's vertical extent and widens or narrows the horizontal to
257
+ * the canvas. `'cover'` always fills the tile with photograph: a canvas WIDER than the capture
258
+ * keeps the width and crops top/bottom (a 4:3 capture in a 16:9 tile); a narrower one is
259
+ * `'height'`. Both backends; the 3D rig's vertical FOV follows the crop. Anything else throws.
260
+ */
261
+ captureFit?: 'height' | 'cover';
161
262
  /** Camera rig only: the distance in world metres that sits ON the glass. */
162
263
  convergence?: number;
163
264
  /**
@@ -189,13 +290,68 @@ export interface SplatOptions {
189
290
  observe?: Element;
190
291
  }
191
292
 
293
+ /** `handle.stats()` on `engine: 'playcanvas'`. */
294
+ export interface SplatStats {
295
+ /** `'streamed'` for a `lod-meta.json`, `'flat'` for `.sog`/`.ply`, null before load. */
296
+ kind: 'flat' | 'streamed' | null;
297
+ /**
298
+ * Splats the engine placed in this tile's work buffer on the LAST frame: after LOD selection
299
+ * and the budget, before per-view frustum culling. Every view of the tile draws from this set.
300
+ */
301
+ resident: number;
302
+ /** The largest `resident` seen so far. */
303
+ peakResident: number;
304
+ /** The tile's splat budget (all views included), or null. */
305
+ budget: number | null;
306
+ /** The asset's own count: every splat of a flat source, the finest level of a Streamed SOG. */
307
+ numSplats: number;
308
+ /** Views drawn last frame (1 in mono, the runtime's view count in 3D). */
309
+ views: number;
310
+ /** Streamed SOG only (null otherwise): LOD levels, chunk files, chunk files currently loaded. */
311
+ lodLevels: number | null;
312
+ files: number | null;
313
+ filesLoaded: number | null;
314
+ /**
315
+ * `performance.now()` (ms since navigation start) of the first frame that drew a non-empty
316
+ * set — the page's time to first splat. Null until then.
317
+ */
318
+ firstFrameMs: number | null;
319
+ }
320
+
192
321
  /** What {@link addSplat} returns: a TileHandle plus the objects behind it. */
193
322
  export interface SplatHandle {
194
- readonly viewer: SceneViewer;
195
- /** Spark's SplatMesh. */
323
+ /**
324
+ * SceneViewer on Spark; the PlayCanvas backend's own viewer on `engine: 'playcanvas'` (null
325
+ * there until the backend module has loaded — one module fetch after addSplat returns).
326
+ */
327
+ readonly viewer: SceneViewer | PlayCanvasSplatViewer;
328
+ /**
329
+ * Spark's SplatMesh; on `engine: 'playcanvas'` a `{ numSplats, entity, asset, resource }`
330
+ * record of the engine objects. Null until the asset is loaded.
331
+ */
196
332
  readonly mesh: object;
197
- /** Spark's SparkRenderer. */
198
- readonly spark: object;
333
+ /** Spark's SparkRenderer (absent on `engine: 'playcanvas'`). */
334
+ readonly spark?: object;
335
+ /** Which backend is rendering: `'playcanvas'` or `'spark'`; null until it has loaded. */
336
+ readonly backend: 'playcanvas' | 'spark' | null;
337
+ /**
338
+ * ADVANCED — not covered by the semver promise. The renderer objects behind this window, for a
339
+ * page that wants to add its own content. Null until the backend has booted.
340
+ *
341
+ * PlayCanvas: `{ app, root, camera }` — the tile's `pc.AppBase`; the content root entity (the
342
+ * splat's content space — add your own entities under it, e.g. a glTF through the engine's
343
+ * container loader, skinned and animated included); the eye-rig camera entity. `remove()`
344
+ * destroys the app, and everything under `root` with it.
345
+ *
346
+ * Spark: `{ renderer, scene, camera }` — the WebGLRenderer, the scene, and whichever camera draws
347
+ * the current frame.
348
+ */
349
+ readonly engine:
350
+ | { readonly app: unknown; readonly root: unknown; readonly camera: unknown }
351
+ | { readonly renderer: unknown; readonly scene: unknown; readonly camera: unknown }
352
+ | null;
353
+ /** `engine: 'playcanvas'` only: the live focus, in the splat's own space. */
354
+ getFocus?(opts?: { target?: boolean }): number[] | null;
199
355
  /** Bounds actually used for framing; null until `ready` resolves. */
200
356
  frame: SubjectBounds | null;
201
357
  /**
@@ -222,8 +378,36 @@ export interface SplatHandle {
222
378
  point: number[] | { x: number; y: number; z: number } | null,
223
379
  opts?: { snap?: boolean },
224
380
  ): SplatHandle;
225
- /** What is under a point on the canvas, in the splat's own space — the double-click's raycast. */
381
+ /**
382
+ * Swap the asset (URL or bytes) in place. PlayCanvas backend only — throws on Spark.
383
+ *
384
+ * The new file loads BEHIND the current one; then the two crossfade over `fadeMs` (0 = a cut)
385
+ * and the old one is released. The rig waterfall re-runs for the new file (rig, lens, focus and
386
+ * frame update; `onFocusChange` fires). The pose (yaw/pitch/zoom/depth) is kept unless
387
+ * `resetPose`. A newer call supersedes an older one still loading. Resolves once the fade has
388
+ * finished; rejects if the new asset cannot be loaded (the current one stays on screen).
389
+ */
390
+ setSource(
391
+ src: string | Blob | ArrayBuffer | Uint8Array,
392
+ opts?: { fadeMs?: number; resetPose?: boolean },
393
+ ): Promise<SplatHandle>;
394
+ /**
395
+ * Called with the live focus (the splat's own space) whenever it moves — easing included — and
396
+ * which waterfall step it came from. PlayCanvas backend; assign any time, even before `ready`.
397
+ */
398
+ onFocusChange: ((point: number[], info: { focusSource: ResolvedRig['focusSource'] | null }) => void) | null;
399
+ /**
400
+ * What is under a point on the canvas, in the splat's own space — the double-click's pick.
401
+ * PlayCanvas: the nearest gaussian CENTRE to the ray over the FULL centre set (haze under 5 %
402
+ * opacity skipped); on a Streamed SOG, over the chunks currently resident. Spark: its surface
403
+ * raycast, falling back to the nearest centre.
404
+ */
226
405
  pick(clientX: number, clientY: number): number[] | null;
406
+ /**
407
+ * `engine: 'playcanvas'` only: splat accounting for this tile. Null until the backend module
408
+ * has loaded.
409
+ */
410
+ stats?(): SplatStats | null;
227
411
 
228
412
  /** Close this window and release its GPU resources. */
229
413
  remove(): void;
package/three.d.ts CHANGED
@@ -100,6 +100,23 @@ export function cameraRigFromCamera(
100
100
  opts?: CameraRigOptions,
101
101
  ): XRViewRigInit;
102
102
 
103
+ /**
104
+ * The same CAMERA-rig descriptor as {@link cameraRigFromCamera}, from a plain pose instead of a
105
+ * three.js camera — for a renderer that is not three. Agrees with cameraRigFromCamera to the bit
106
+ * for the same pose.
107
+ */
108
+ export function cameraRigFromPose(
109
+ pose: {
110
+ /** WORLD position. */
111
+ position: { x: number; y: number; z: number };
112
+ /** WORLD orientation quaternion. */
113
+ orientation: { x: number; y: number; z: number; w: number };
114
+ /** FULL vertical angle in DEGREES (three's `camera.fov` convention). */
115
+ fov: number;
116
+ },
117
+ opts?: CameraRigOptions,
118
+ ): XRViewRigInit;
119
+
103
120
  /**
104
121
  * Build a DISPLAY-rig descriptor — the default rig, made explicit and posable: the canvas is a
105
122
  * portal onto a virtual display `virtualDisplayHeight` metres tall. Adds what the scalar