@displayxr/inline3d 1.14.0 → 1.16.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.
@@ -373,6 +373,13 @@ export function addSplat(wall, canvas, src, opts = {}) {
373
373
  prepareSource() {
374
374
  throw new Error("@displayxr/inline3d/splat: prepareSource() is implemented on the PlayCanvas backend only.");
375
375
  },
376
+ /** Not on this backend: the live rig switch is a PlayCanvas-backend feature. */
377
+ setRig() {
378
+ throw new Error(
379
+ "@displayxr/inline3d/splat: setRig() is implemented on the PlayCanvas backend only " +
380
+ "(addSplat(…, { engine:'playcanvas' })).",
381
+ );
382
+ },
376
383
  /** Not on this backend (splat effects are PlayCanvas-only in this version). */
377
384
  playEffect() {
378
385
  throw effectsNotOnSpark('playEffect()');
@@ -657,6 +664,16 @@ export function addSplat(wall, canvas, src, opts = {}) {
657
664
  * Fields (`viewer`, `mesh`, `rig`, …) are null until then; `viewer` in particular appears one
658
665
  * module-load later than on Spark.
659
666
  */
667
+ /** setRig's type/options shape, checked at the call before the adapter has loaded (it re-checks all of it). */
668
+ function validateSetRigArgs(type, o) {
669
+ if (!['display', 'camera', 'auto'].includes(type)) {
670
+ throw new Error(`@displayxr/inline3d/splat: setRig("${type}") — expected 'display', 'camera' or 'auto'.`);
671
+ }
672
+ if (o !== undefined && (o === null || typeof o !== 'object')) {
673
+ throw new TypeError('@displayxr/inline3d/splat: setRig options must be an object.');
674
+ }
675
+ }
676
+
660
677
  function addSplatDeferred(wall, canvas, src, opts) {
661
678
  const pending = [];
662
679
  const queue = (name) => (...args) => {
@@ -711,6 +728,14 @@ function addSplatDeferred(wall, canvas, src, opts) {
711
728
  // setSource replaces this stub on the same object by then).
712
729
  setSource: (...args) => out.ready.then(() => out.setSource(...args)),
713
730
  prepareSource: (...args) => out.ready.then(() => out.prepareSource(...args)),
731
+ // A rig switch before the first asset has landed: validated now (controls:'page' and a bad
732
+ // type throw at the call's own line), applied once it has.
733
+ setRig: page
734
+ ? pageOnly('setRig')
735
+ : (type, o) => {
736
+ validateSetRigArgs(type, o);
737
+ return out.ready.then(() => out.setRig(type, o));
738
+ },
714
739
  // Effects before the adapter has loaded: validated NOW (a bad call throws at its own line),
715
740
  // then run once the first asset is on screen.
716
741
  playEffect: (name, o) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.14.0",
3
+ "version": "1.16.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",
package/splat.d.ts CHANGED
@@ -131,7 +131,8 @@ export interface SogCamera {
131
131
  */
132
132
  export interface ResolvedRig {
133
133
  type: 'camera' | 'display';
134
- typeSource: 'caller' | 'block' | 'block-present' | 'default';
134
+ /** `'setRig'` after `handle.setRig('display' | 'camera')`; the waterfall's step otherwise. */
135
+ typeSource: 'caller' | 'block' | 'block-present' | 'default' | 'setRig';
135
136
  rest: { position: number[]; rotation: number[] };
136
137
  intrinsics: SogIntrinsics;
137
138
  intrinsicsSource: 'block' | 'caller' | 'estimated' | 'fallback-28mm';
@@ -154,7 +155,14 @@ export interface ResolvedRig {
154
155
  | 'median-disparity'
155
156
  | 'default'
156
157
  | 'picked'
157
- | 'set';
158
+ | 'set'
159
+ | 'frame';
160
+ /**
161
+ * On a display rig set by `handle.setRig('display')`: what it framed, in `handle.engine.root`'s
162
+ * space, and where that came from (the page's meshes, meshes + the shown splat, the splat alone,
163
+ * or the caller's `frame`). Absent otherwise.
164
+ */
165
+ frame?: { center: number[]; extent: number[]; source: 'root' | 'root+splat' | 'splat' | 'caller' } | null;
158
166
  /** The block's own `focus.source` string (e.g. `'convergence'`, `'cloud-median'`), for diagnostics. */
159
167
  blockFocusSource: string | null;
160
168
  /** Fraction of the central crop's opacity-weighted mass the winning clump carried (`nearest-clump` only). */
@@ -187,6 +195,13 @@ export interface SplatOptions {
187
195
  * the knob for the weave's zero-copy read race on large canvases.
188
196
  */
189
197
  preserveDrawingBuffer?: boolean;
198
+ /**
199
+ * `engine: 'playcanvas'` only: create the tile's WebGL context with MSAA (default false — it buys
200
+ * nothing on alpha-blended splats). Turn it on when the page draws meshes under
201
+ * `handle.engine.root` and wants addModel's silhouettes (addModel's context has MSAA on);
202
+ * `setRig('display')` is then pixel-identical to an addModel tile. Fixed at context creation.
203
+ */
204
+ antialias?: boolean;
190
205
  /**
191
206
  * `engine: 'playcanvas'` only: the `playcanvas` module namespace to use instead of
192
207
  * `import('playcanvas')` — for a page that already bundles its own copy.
@@ -374,6 +389,10 @@ export interface SplatParticleOptions {
374
389
  * home; max 0.05). In 2D a nominal 64 mm-at-1.7 m separation is assumed.
375
390
  */
376
391
  maxDisparity?: number;
392
+ /** Fade a particle over the first `vanish` of its flight (0 = off, the reveals' default; the particle transitions use ~0.3–0.45). */
393
+ vanish?: number;
394
+ /** Share of the gaussians drawn while in flight, 0..1 (1 = all, the reveals' default; the swarm uses 0.2). */
395
+ density?: number;
377
396
  /** `order: 'layers'`: gaussians per layer (default 768² = 589 824, SHARP). */
378
397
  layerSize?: number;
379
398
  /** `assemble`: how far out the swarm starts (half-view-widths), its spiral (rad), its curl turbulence, how coherent its start field is (0..1), and how much of it reaches back in depth (0..1). */
@@ -486,6 +505,46 @@ export interface SplatEffectState {
486
505
  progress: number;
487
506
  }
488
507
 
508
+ /**
509
+ * `handle.setRig('display', opts)`'s options. Every default is addModel's, so a mesh framed this
510
+ * way renders exactly as `addModel(url, { engine: 'playcanvas' })` renders it.
511
+ * docs/playcanvas-adapter.md §setRig.
512
+ */
513
+ export interface SplatSetRigDisplayOptions {
514
+ /** Default 0.24. */
515
+ virtualDisplayHeight?: number;
516
+ /** Default `'contain'`. */
517
+ fit?: 'contain' | 'cover' | 'height' | 'none';
518
+ /** Default 0.8. */
519
+ margin?: number;
520
+ /** Default 4. */
521
+ depthLimit?: number;
522
+ /** Default true. */
523
+ fitSweep?: boolean;
524
+ /** Idle turntable, °/s after 2.5 s without input. Default 8 (addModel's); 0 for a still product. */
525
+ idleSpin?: number;
526
+ /** On the declared display rig. Default 1 each. */
527
+ ipdFactor?: number;
528
+ parallaxFactor?: number;
529
+ perspectiveFactor?: number;
530
+ /**
531
+ * The eye camera's tone mapping WHILE THE SPLAT IS HIDDEN (the splat's display-referred colours
532
+ * keep `'none'` whenever it is shown). Default `'neutral'` — addModel's (Khronos PBR Neutral).
533
+ */
534
+ toneMapping?: 'none' | 'linear' | 'neutral' | 'aces' | 'aces2' | 'filmic' | 'hejl';
535
+ /**
536
+ * `'room'` (default; `'neutral'` is the same): install addModel's generated neutral-studio IBL
537
+ * when the scene has no `envAtlas` of its own, removed again when the rig switches away. A page
538
+ * that lights its own meshes is never touched. `'none'`: never install it.
539
+ */
540
+ environment?: 'room' | 'neutral' | 'none';
541
+ /**
542
+ * Frame THIS box (in `handle.engine.root`'s space) instead of measuring. Default: the enabled
543
+ * meshes under root (+ the splat's box when it is shown); with no meshes, the splat itself.
544
+ */
545
+ frame?: { center: number[] | { x: number; y: number; z: number }; extent: number[] | { x: number; y: number; z: number } };
546
+ }
547
+
489
548
  /** `setSource`'s options. */
490
549
  export interface SplatSourceOptions {
491
550
  /** > 0 = a crossfade of this length (the 1.10 option; same as `transition: 'crossfade'`). */
@@ -497,8 +556,15 @@ export interface SplatSourceOptions {
497
556
  * inflates out of its own; default 2200 ms), `'wavefront'` (a soft front crosses left → right
498
557
  * with a thin depth ridge riding it; default 2000 ms, ease-in-out; falls back to the crossfade
499
558
  * in a hidden tab). For photo slideshows: `crossfade` or `wavefront`.
500
- */
501
- transition?: 'cut' | 'crossfade' | 'flip' | 'wavefront';
559
+ *
560
+ * The particle transitions (docs/splat-effects.md §Particle transitions): `'swarm'` (the old
561
+ * photo disperses into a curl-noise swarm while the new one assembles out of one), `'burst'`
562
+ * (the old one collapses into its focus, the new one bursts out of its own), `'shimmer-cross'`
563
+ * (nothing moves: twinkling points out, twinkling points in) and `'dust'` (drifting dust out,
564
+ * gathering dust in). 2.6–2.8 s, linear shared clock (each particle eases itself). The old
565
+ * photo is live, in 2D too; a hidden tab gets the crossfade.
566
+ */
567
+ transition?: 'cut' | 'crossfade' | 'flip' | 'wavefront' | SplatParticleTransitionName;
502
568
  durationMs?: number;
503
569
  easing?: SplatEasing;
504
570
  /** `cut`/`crossfade` only: reveal the INCOMING asset (entity scope) while the old one fades. */
@@ -519,8 +585,34 @@ export interface SplatSourceOptions {
519
585
  * the single-camera RenderView path always use `'frozen'`.
520
586
  */
521
587
  outgoing?: 'live' | 'frozen';
588
+ /** Particle transitions: particle options for BOTH photos (the shared ones of the particle reveals). */
589
+ order?: SplatParticleOptions['order'];
590
+ stagger?: number;
591
+ jitter?: number;
592
+ maxDisparity?: number;
593
+ dotSize?: number;
594
+ noiseScale?: number;
595
+ layerSize?: number;
596
+ origin?: SplatEffectOrigin;
597
+ /** Particle transitions: how much of the clock the two photos' spans share, 0..1 (0 = one after the other). */
598
+ overlap?: number;
599
+ /** Particle transitions: option overrides for the OUTGOING photo's effect. */
600
+ outgoingFx?: SplatParticleOptions & Record<string, unknown>;
601
+ /** Particle transitions: option overrides for the INCOMING photo's effect. */
602
+ incomingFx?: SplatParticleOptions & Record<string, unknown>;
522
603
  }
523
604
 
605
+ /**
606
+ * `prepareSource`'s options: the `setSource` options the prepared asset will be swapped in with.
607
+ * For a particle transition, its shaders are compiled and linked NOW, in the dwell — otherwise the
608
+ * first transition of each kind in a page blocks its first frame on the link (tens of ms, hundreds
609
+ * the first time a machine sees the variant). Validated like setSource's.
610
+ */
611
+ export type SplatPrepareOptions = SplatSourceOptions;
612
+
613
+ /** setSource's particle transitions (docs/splat-effects.md §Particle transitions). */
614
+ export type SplatParticleTransitionName = 'swarm' | 'burst' | 'shimmer-cross' | 'dust';
615
+
524
616
  /**
525
617
  * What `prepareSource()` resolves to: an opaque, single-use handle for `setSource`. The asset is
526
618
  * fully resident (GPU textures + the engine's centre array) until used or disposed.
@@ -689,7 +781,20 @@ export interface SplatHandle {
689
781
  * Memory: two full assets resident until the swap (≈ +70 MB of GPU textures per 1.18M-gaussian
690
782
  * SOG). `remove()` disposes any still unused.
691
783
  */
692
- prepareSource(src: string | Blob | ArrayBuffer | Uint8Array): Promise<SplatPreparedSource>;
784
+ prepareSource(src: string | Blob | ArrayBuffer | Uint8Array, opts?: SplatPrepareOptions): Promise<SplatPreparedSource>;
785
+ /**
786
+ * PlayCanvas backend only — throws on Spark, and with `controls:'page'` (the page's camera IS
787
+ * the rig). Switch the rig live, on the next frame: no remount, no reload, no new session.
788
+ *
789
+ * `'display'` frames the page's meshes under `handle.engine.root` (+ the splat when shown) on
790
+ * addModel's defaults and declares a display rig; `'camera'` returns to the asset's capture rig,
791
+ * re-resolved from the waterfall's own inputs (lens, captureFit, focus rung); `'auto'` is what
792
+ * the load resolved. The pose resets to the new rig's rest; the declared view rig switches with
793
+ * it; the choice is sticky across `setSource` until `'auto'`. A clean cut (no eased transition).
794
+ * `handle.rig.typeSource` reads `'setRig'`. Resolves once applied. docs/playcanvas-adapter.md §setRig.
795
+ */
796
+ setRig(type: 'display', opts?: SplatSetRigDisplayOptions): Promise<SplatHandle>;
797
+ setRig(type: 'camera' | 'auto'): Promise<SplatHandle>;
693
798
  /**
694
799
  * `engine: 'playcanvas'` only (throws on Spark). Play a transition/pulse/custom effect;
695
800
  * validated at the call, run once the first asset is on screen. Resolves `{ finished }` —