@displayxr/inline3d 1.36.0 → 1.37.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/cursor-depth.d.ts +39 -2
- package/dist/call.js +1 -1
- package/js/inline3d-cursor-depth-playcanvas.js +74 -18
- package/js/inline3d-cursor-depth.js +173 -18
- package/js/inline3d-cursor-option.js +38 -0
- package/js/inline3d-model.js +3 -1
- package/js/inline3d-playcanvas-engine.js +1 -0
- package/js/inline3d-splat-playcanvas.js +5 -3
- package/js/inline3d-splat.js +3 -1
- package/js/inline3d-three.js +42 -20
- package/model.d.ts +4 -2
- package/package.json +2 -1
- package/splat.d.ts +4 -2
- package/three.d.ts +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
|
|
|
5
5
|
`./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
|
|
6
6
|
which tier they touch, because that is what tells you whether an upgrade can move your pixels.
|
|
7
7
|
|
|
8
|
+
## 1.37.0 — 2026-10-07
|
|
9
|
+
|
|
10
|
+
Driven by a tester's run on a head-tracked panel (the depth cursor shipped in 1.36.0). Additive
|
|
11
|
+
API, plus two deliberate default changes to the cursor, which is opt-in, so nothing changes for a
|
|
12
|
+
page that does not use it.
|
|
13
|
+
|
|
14
|
+
### Depth cursor: parallax, margin, overlays, visibility
|
|
15
|
+
|
|
16
|
+
- **The cursor now parallaxes like the content (new default `anchor: 'hybrid'`).** 1.36.0 kept
|
|
17
|
+
the sprite on the line of sight through the pointer. Its image on the glass then never moved
|
|
18
|
+
with the head: disparity without motion parallax. On a head-tracked display that reads as "at
|
|
19
|
+
the glass", behind the content it should sit on.
|
|
20
|
+
- **Hybrid:** follows the pointer exactly while it moves, and stays world-fixed while the
|
|
21
|
+
pointer is still, so it parallaxes with the content as you look around. It is continuous at
|
|
22
|
+
the moment the pointer stops.
|
|
23
|
+
- **`anchor: 'screen'`:** the 1.36 behaviour.
|
|
24
|
+
- **`anchor: 'world'`:** always straight in front of the pointer's canvas point.
|
|
25
|
+
- **Default margin 0.03 → 0.005** (eye-baseline units, about 1.5 mm at 60 cm). The old default
|
|
26
|
+
floated 1–2 cm off shallow content; the cursor now rests on what it hovers. There's also a
|
|
27
|
+
`margin` option.
|
|
28
|
+
- **`pointerScope: 'window'`:** the cursor stays active over DOM layered on the canvas (overlay
|
|
29
|
+
buttons), and the CSS cursor is hidden page-wide while it shows.
|
|
30
|
+
- **Filled, outlined crosshair** instead of 1 px lines, which read too thin through the lens.
|
|
31
|
+
Both engines draw it the same way.
|
|
32
|
+
- **`./model` / `./splat`:** `cursor` also takes an object, `{ margin, anchor, height,
|
|
33
|
+
pointerScope }`, validated at the call.
|
|
34
|
+
- **`./cursor-depth`:** `CURSOR_ANCHOR_MODES`, `CursorOptions`, `resolveCursorOption`,
|
|
35
|
+
`cursorCrosshairMesh`, `cursorModelMatrix`; `CursorDepthPlacer` takes `{ anchor }`.
|
|
36
|
+
- **Docs:** `hitTest` must be cheap and cover everything that sticks out.
|
|
37
|
+
- **e2e:** `test:e2e:cursor` adds a head-move check on the real viewers. Hybrid and world stay
|
|
38
|
+
world-fixed and screen slides; this holds on both engines, for models and splats.
|
|
39
|
+
|
|
8
40
|
## 1.36.0 — 2026-10-07
|
|
9
41
|
|
|
10
42
|
Additive everywhere it touches: the **core** `./three` (new helpers, minor per
|
package/cursor-depth.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface CursorTuning {
|
|
|
18
18
|
stale: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/** The runtime's defaults: margin 0.
|
|
21
|
+
/** The runtime's defaults: margin 0.005 (~1.5 mm at 60 cm), clamp ±0.6, rise 30 ms, sink 250 ms, stale 0.5 s. */
|
|
22
22
|
export const CURSOR_DEFAULT_TUNING: Readonly<CursorTuning>;
|
|
23
23
|
/** Default sprite height, as a fraction of the canvas height (0.03). */
|
|
24
24
|
export const CURSOR_DEFAULT_HEIGHT: number;
|
|
@@ -47,6 +47,41 @@ export function cursorFilterStep(filter: object, tuning: CursorTuning, target: n
|
|
|
47
47
|
/** Where to draw a sprite at `disparity`, and how tall. */
|
|
48
48
|
export function placeCursor(g: CursorGeometry, disparity: number, heightFraction: number): { position: number[]; height: number };
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Where along the cursor's depth the sprite goes.
|
|
52
|
+
* - `'hybrid'` (default): on the line of sight while the pointer MOVES (exactly over it), and
|
|
53
|
+
* world-FIXED while it is still, so it parallaxes with the content as the head moves.
|
|
54
|
+
* - `'screen'`: always on the line of sight. Never parallaxes; on a head-tracked display it can
|
|
55
|
+
* read as "at the glass".
|
|
56
|
+
* - `'world'`: always straight in front of the pointer's canvas point. Parallaxes; drifts ~mm off
|
|
57
|
+
* the pointer from an off-axis viewer.
|
|
58
|
+
*/
|
|
59
|
+
export type CursorAnchorMode = 'hybrid' | 'screen' | 'world';
|
|
60
|
+
export const CURSOR_ANCHOR_MODES: readonly CursorAnchorMode[];
|
|
61
|
+
|
|
62
|
+
/** `cursor: { … }` on ./model and ./splat (an object also means `'depth'`). */
|
|
63
|
+
export interface CursorOptions {
|
|
64
|
+
/** Sprite height as a fraction of the canvas height (default 0.03). */
|
|
65
|
+
height?: number;
|
|
66
|
+
/** How far in front of the content it floats, in eye-baseline units (default 0.005). */
|
|
67
|
+
margin?: number;
|
|
68
|
+
/** Default `'hybrid'`. */
|
|
69
|
+
anchor?: CursorAnchorMode;
|
|
70
|
+
/** `'window'`: keep the cursor over DOM layered on the canvas, and hide the CSS cursor page-wide meanwhile. Default `'canvas'`. */
|
|
71
|
+
pointerScope?: 'canvas' | 'window';
|
|
72
|
+
}
|
|
73
|
+
export const CURSOR_OPTION_KEYS: readonly (keyof CursorOptions)[];
|
|
74
|
+
/** `cursor` as ./model and ./splat accept it: null = off; throws on a bad value. */
|
|
75
|
+
export function resolveCursorOption(opt: unknown, who?: string): CursorOptions | null;
|
|
76
|
+
|
|
77
|
+
/** The crosshair as filled, outlined strokes in its own XY plane (unit = sprite height), non-indexed triangles. */
|
|
78
|
+
export function cursorCrosshairMesh(
|
|
79
|
+
fill?: readonly number[],
|
|
80
|
+
outline?: readonly number[],
|
|
81
|
+
): { positions: Float32Array; colors: Float32Array; count: number };
|
|
82
|
+
/** Column-major model matrix placing the unit crosshair for a placement. */
|
|
83
|
+
export function cursorModelMatrix(placement: CursorPlacement, out?: number[]): number[];
|
|
84
|
+
|
|
50
85
|
/** One frame's placement. `position`/`basis` are in the views' space; null when inactive. */
|
|
51
86
|
export interface CursorPlacement {
|
|
52
87
|
active: boolean;
|
|
@@ -55,11 +90,13 @@ export interface CursorPlacement {
|
|
|
55
90
|
height: number;
|
|
56
91
|
disparity: number;
|
|
57
92
|
targetDisparity: number;
|
|
93
|
+
/** Hybrid: true while the pointer is still and the sprite is held world-fixed. */
|
|
94
|
+
anchored?: boolean;
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
/** The whole pipeline with its filter state: one per cursor. Inactive with fewer than two views. */
|
|
61
98
|
export class CursorDepthPlacer {
|
|
62
|
-
constructor(tuning?: CursorTuning);
|
|
99
|
+
constructor(tuning?: CursorTuning, opts?: { anchor?: CursorAnchorMode });
|
|
63
100
|
update(
|
|
64
101
|
views: ArrayLike<CursorView | XRView>,
|
|
65
102
|
hint: {
|
package/dist/call.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* @displayxr/inline3d 1.
|
|
1
|
+
/* @displayxr/inline3d 1.37.0 — dist/call.js — Apache-2.0 — https://github.com/DisplayXR/displayxr-web */
|
|
2
2
|
var _e=Object.freeze({speaker:"high",other:"normal",hidden:"paused"}),Cn=["high","normal","low","paused"],Rn=.02,Dn=1e3/20,Ln=3e3,An=1e3/28;function ye(n){return typeof n=="function"?n:n===!1||n==="off"?"off":"auto"}function it(n=import.meta.url){return ge?typeof ge=="function"?ge():ge:new URL("../lift/index.js",n).href}var ge=null;function ii(n){ge=n||null}function On(){return import(it())}async function nt(n,{importer:e=On,nav:t=globalThis.navigator,log:i=null,capsOpts:r={webFallback:!0}}={}){let s=(h,d)=>i&&i(h,d),o=(h,d={})=>(s("lift-unavailable",{reason:h,...d}),{lift:null,capabilities:null,caps:null,source:"none",reason:h}),a=ye(n);if(a==="off")return{lift:null,capabilities:null,caps:null,source:"none",reason:"off"};if(typeof a=="function"){let h=typeof a.liftCapabilities=="function"?a.liftCapabilities:null;return{lift:a,capabilities:h,caps:null,source:"injected",reason:null}}let l;try{l=await e()}catch(h){return o("import-failed",{message:String(h&&h.message||h).slice(0,200)})}let c=l&&typeof l.lift=="function"?l.lift:null;if(!c)return o("no-lift-export");let p=typeof l.liftCapabilities=="function"?l.liftCapabilities:null,f=null;if(p){try{f=await p(r)}catch{f=null}let h=!!(f&&f.webFallback&&f.webFallback.webgpu);if(f&&!f.native&&!h)return o("no-provider",{native:!1,webgpu:!1})}else if(!(t&&t.gpu))return o("no-webgpu");return s("lift-ready",{native:!!(f&&f.native),provider:f&&f.provider||null}),{lift:c,capabilities:p,caps:f,source:"module",reason:null}}function ve(n){let e=Number.isFinite(+n)&&n!==null?Math.max(-1,Math.min(1,+n)):0;return Math.abs(e)<Rn?"auto":Math.round((.5+.5*e)*1e3)/1e3}function Le({id:n,speakerId:e,visible:t=!0}){return t?n!=null&&n===e?_e.speaker:_e.other:_e.hidden}function rt(n,e){if(!n||!Cn.includes(e)||typeof n.setPriority!="function")return!1;try{return n.setPriority(e)!==!1}catch{return!1}}function st({degradedMs:n=Dn,recoveredMs:e=An,holdMs:t=Ln,alpha:i=.1}={}){let r=null,s=0,o=!1;return{feed(a){if(!(a>0)||a>1e3)return null;if(r=r===null?a:r+i*(a-r),o){if(r<e)return o=!1,s=0,"recovered"}else if(s=r>n?s+a:0,s>=t)return o=!0,"degraded";return null},reset(){r=null,s=0,o=!1},get frameMs(){return r},get degraded(){return o}}}function ot({lift:n,max:e=4,log:t=null,options:i=null}={}){let r=i&&typeof i=="object"?i:{},s=new Map,o=null,a=0,l=0,c=(h,d)=>t&&t(h,d),p=(h,d)=>{if(!d.handle)return;let m=Le({id:h,speakerId:o,visible:d.visible});if(m===d.priority)return;d.priority=m;let u=rt(d.handle,m);c("lift-priority",{peer:h,priority:m,applied:u})},f={get size(){return s.size},get max(){return e},set max(h){e=Math.max(0,h|0)},has:h=>s.has(h),handle:h=>s.get(h)&&s.get(h).handle||null,priority:h=>s.get(h)&&s.get(h).priority||null,canAcquire:h=>s.has(h)||s.size<e,acquire(h,d,{wall:m,visible:u=!0,onError:v}={}){let y=s.get(h);if(y)return y.pending||Promise.resolve(y.handle);if(s.size>=e)return Promise.resolve(null);let b=++l,C=Le({id:h,speakerId:o,visible:u}),x={gen:b,handle:null,visible:u,priority:C,pending:null};s.set(h,x);let I={quality:"auto",...r,mode:"live",wall:m,ui:"none",convergence:ve(a),priority:C};return c("lift-acquire",{peer:h,priority:C,convergence:I.convergence}),x.pending=Promise.resolve().then(()=>n(d,I)).then(T=>{if(s.get(h)!==x||x.gen!==b){try{T&&T.remove&&T.remove()}catch{}return null}return x.pending=null,x.handle=T||null,x.priority=null,p(h,x),T&&typeof T.setConvergence=="function"&&T.setConvergence(ve(a)),x.handle},T=>(s.get(h)===x&&s.delete(h),c("lift-failed",{peer:h,message:String(T&&T.message||T).slice(0,200)}),v&&v(T),null)),x.pending},release(h){let d=s.get(h);if(d){if(s.delete(h),d.handle)try{d.handle.remove()}catch{}c("lift-release",{peer:h})}},releaseAll(){for(let h of[...s.keys()])f.release(h)},setSpeaker(h){o=h===void 0?null:h;for(let[d,m]of s)p(d,m)},setVisible(h,d){let m=s.get(h);!m||m.visible===!!d||(m.visible=!!d,p(h,m))},setDepth(h){a=h;let d=ve(h);for(let m of s.values())m.handle&&typeof m.handle.setConvergence=="function"&&m.handle.setConvergence(d);return d},get anyWeb(){for(let h of s.values())if(h.handle&&!h.handle.native)return!0;return!1},list(){return[...s].map(([h,d])=>({id:h,priority:d.priority,pending:!!d.pending,native:!!(d.handle&&d.handle.native)}))}};return f}var Pn=null;function ni(n){Pn=typeof n=="function"?n:null}var Fn=new WeakMap;function ri(n,e){n&&(typeof n=="object"||typeof n=="function")&&Fn.set(n,e)}var jn=Object.freeze({displaySizeM:Object.freeze([.3456,.1944]),displayPixels:Object.freeze([1920,1080]),nominalViewerM:.65,eyeSeparationM:.063});function si(n,e,t=1){let i=jn,r=!!n&&n.displayWidthMeters>0&&n.displayHeightMeters>0&&n.displayPixelWidth>0&&n.displayPixelHeight>0,s=r?[n.displayWidthMeters,n.displayHeightMeters]:[...i.displaySizeM],o=r?[n.displayPixelWidth,n.displayPixelHeight]:[...i.displayPixels],a=(s[0]/o[0]+s[1]/o[1])/2,l=t>0?t:1,c=r?a*l:a*(o[1]/Math.max(1,globalThis.innerHeight||o[1])),p=n?.nominalViewerPosition?.z,f=Number.isFinite(p)&&p>.1;return{canvasSizeM:[(e?.width||0)*c,(e?.height||0)*c],metersPerCssPx:c,displaySizeM:s,nominalViewerM:f?p:i.nominalViewerM,eyeSeparationM:i.eyeSeparationM,source:{size:r?"display":"default",viewer:f?"display":"default",eyeSeparation:"default"}}}var oi=new WeakMap,J;function ai(n){let e=null;for(let[t,i]of n)i>0&&(!e||i<e[1])&&(e=[t,i]);return e}function Wn(n){if(!n||typeof n.getParameter!="function")return null;let e=oi.get(n);if(e)return e;let t,i,r;try{t=n.getParameter(n.MAX_TEXTURE_SIZE??3379),i=n.getParameter(n.MAX_RENDERBUFFER_SIZE??34024),r=n.getParameter(n.MAX_VIEWPORT_DIMS??3386)}catch{return null}let s=r&&r.length>=2?r[0]:0,o=r&&r.length>=2?r[1]:0,a=ai([["MAX_TEXTURE_SIZE",t],["MAX_RENDERBUFFER_SIZE",i],["MAX_VIEWPORT_DIMS",s]]),l=ai([["MAX_TEXTURE_SIZE",t],["MAX_RENDERBUFFER_SIZE",i],["MAX_VIEWPORT_DIMS",o]]);if(!a||!l)return null;let c={maxW:a[1],maxH:l[1],nameW:a[0],nameH:l[0],valueW:a[1],valueH:l[1]};return oi.set(n,c),c}function li(){if(J!==void 0)return J;J=null;try{if(typeof document>"u"||!document||typeof document.createElement!="function")return J;let n=document.createElement("canvas"),e=n.getContext("webgl2")||n.getContext("webgl");if(!e)return J;J=Wn(e),e.getExtension?.("WEBGL_lose_context")?.loseContext?.()}catch{J=null}return J}function ci(n,e,t,{cols:i=2,rows:r=1}={}){let s=n*i,o=e*r,a={eyeW:n,eyeH:e,cols:i,scale:1,clamped:!1,reqW:s,reqH:o,bufW:s,bufH:o,limitName:"",limitValue:0};if(!t||!(n>0)||!(e>0))return a;let l=t.maxW/s,c=t.maxH/o,p=Math.min(l,c);if(!(p<1))return a;let f=l<=c,h=Math.max(1,Math.min(Math.floor(n*p+1e-9),Math.floor(t.maxW/i))),d=Math.max(1,Math.min(Math.floor(e*p+1e-9),Math.floor(t.maxH/r)));return{eyeW:h,eyeH:d,cols:i,scale:p,clamped:!0,reqW:s,reqH:o,bufW:h*i,bufH:d*r,limitName:f?t.nameW:t.nameH,limitValue:f?t.valueW:t.valueH}}function at(n){return Math.round(n*1e3)/1e3}function di(n,e,t){let i=typeof t=="number"?` (renderScale ${at(t)} \u2192 ${at(t*e.scale)})`:` (scale ${at(e.scale)})`;return`${n} ${e.cols===1?"buffer":"SBS buffer"} ${e.reqW}\xD7${e.reqH} exceeds ${e.limitName} ${e.limitValue} on this device; rendering at ${e.bufW}\xD7${e.bufH}${i}.`}function hi(n,e){let t=n?.width||0,i=n?.height||0,r=e&&e.drawingBufferWidth>0?e.drawingBufferWidth:t,s=e&&e.drawingBufferHeight>0?e.drawingBufferHeight:i,o=t>0?r/t:1,a=i>0?s/i:1;return{sx:o,sy:a,w:r,h:s,mismatch:o!==1||a!==1}}function fi(n,e,t,i="laying the eyes out from the drawing buffer. Lower renderScale to stay inside the device limit."){return`${n} the browser clamped this canvas's drawing buffer to ${t.w}\xD7${t.h} (canvas.width/height say ${e.width}\xD7${e.height}); ${i}`}var ne="smoothstep",ct=["linear","smoothstep","easeoutcubic"];function dt(n){if(typeof n!="string")return null;let e=n.toLowerCase().replace(/[-_\s]/g,"");return ct.includes(e)?e:null}function Nn(n,e){if(e<=0)return 0;if(e>=1)return 1;if(n==="linear")return e;if(n==="easeoutcubic"){let t=1-e;return 1-t*t*t}return e*e*(3-2*e)}var ie="idle",ui="rampDownThenFire",lt="fireThenRampUp",Ae=class{constructor(e,t){this._phase=ie,this._targetMode=null,this._firePending=!1,this._fireAtEnd=!1,this._from=0,this._to=0,this._cur=0,this._t=1,this._dur=180/1e3,this._easing=ne,(e!==void 0||t!==void 0)&&this.configure(e,t)}configure(e,t){if(e!==void 0){let i=Number(e);this._dur=Number.isFinite(i)&&i>0?i:0}return t!==void 0&&(this._easing=dt(t)||ne),this}get durationS(){return this._dur}get easing(){return this._easing}request({targetMode:e=null,targetViewCount:t,currentMode:i=null,currentViewCount:r,current:s,steady:o=1}={}){let a=!(t>1),l=!(r>1),c=Number.isFinite(o)?o:1,p=Number.isFinite(s)?s:c;return this._targetMode=e,this._from=p,a&&!l?(this._phase=ui,this._to=0,this._fireAtEnd=!0,this._firePending=!1):!a&&l?(this._phase=lt,this._from=0,this._to=c,this._firePending=!0,this._fireAtEnd=!1):(this._phase=lt,this._to=a?p:c,this._firePending=!(e!==null&&e===i),this._fireAtEnd=!1),this._t=this._dur>0?0:1,this._cur=this._from,this}update(e){let t=!1;if(this._phase!==ie){let i=Number.isFinite(e)&&e>0?e:0;this._t<1&&this._dur>0?(this._t+=i/this._dur,this._t>1&&(this._t=1)):this._t=1,this._cur=this._from+(this._to-this._from)*Nn(this._easing,this._t),this._phase===lt?(this._firePending&&(this._firePending=!1,t=!0),this._t>=1&&(this._phase=ie)):this._t>=1&&(this._fireAtEnd&&(this._fireAtEnd=!1,t=!0),this._phase=ie)}return{factor:this._cur,fire:t,mode:this._targetMode}}active(){return this._phase!==ie}value(){return this._cur}firePending(){return this._phase===ui?this._fireAtEnd:this._firePending}cancel(){return this._phase=ie,this._firePending=!1,this._fireAtEnd=!1,this._t=1,this}};var U=null;ni(n=>{let e=U;if(!e||!e._running||!n)return null;let t=i=>({layer:i.layer,session:e.session});for(let i of e._windows.values())if(i.canvas===n&&i.layer)return t(i);for(let i of e._windows.values())if(i.layer&&typeof n.contains=="function"&&n.contains(i.canvas))return t(i);for(let i of e._windows.values()){let r=i.canvas.parentElement;if(i.layer&&r&&typeof r.contains=="function"&&r.contains(n))return t(i)}return null});var zn=()=>typeof navigator<"u"&&!!navigator.xr,Q=()=>typeof window<"u"&&typeof window.XRDisplayLayer=="function",Oe=()=>Q()&&"excludeElement"in window.XRDisplayLayer.prototype,ht=()=>{if(!Q())return!1;try{return"setViewRig"in window.XRDisplayLayer.prototype}catch{return!1}},Hn=["getDisplayInfo","getRenderingModes","requestRenderingMode"],Si=["renderingmodechange","hardwaredisplaystatechange"],gt="trackingstatechange",Mi=["unknown","tracking","searching"],mi=[...Si,gt];function Ie(n){try{let e=n?n.trackingState:void 0;return Mi.includes(e)?e:null}catch{return null}}var ft=()=>{if(!Q())return!1;try{let n=window.XRDisplayLayer.prototype;return Hn.every(e=>typeof n[e]=="function")}catch{return!1}},ut=()=>{if(!Q())return!1;try{return typeof window.XRDisplayLayer.prototype.undock=="function"}catch{return!1}},se=null;function re(){if(se!==null)return se;if(!Q())return!1;try{let n=window.XRDisplayLayer.occlusionByDrawOrder;if(typeof n=="boolean")return se=n}catch{}return!1}function Vn(n){if(se!==null)return se;if(!n||!Q()||!("occlusionByDrawOrder"in window.XRDisplayLayer.prototype))return null;try{return se=!!n.occlusionByDrawOrder}catch{return null}}var gi=!1;function pt(){gi||(gi=!0,console.info("[inline3d] This browser composites 2D over woven 3D automatically, per-pixel by draw order \u2014 overlay exclusion is obsolete here, so exclude()/addGlobalOverlay()/data-inline3d-overlay/autoChrome are accepted and ignored. Your 2D chrome already occludes the tiles correctly. The calls are harmless (keep them if you also ship to older DisplayXR Browsers); gate on inline3dOcclusionByDrawOrder() to drop them."))}function Ei(){return zn()&&Q()}function mt(n){return{type:"display",position:{x:0,y:0,z:0},orientation:{x:0,y:0,z:0,w:1},virtualDisplayHeight:n.virtualDisplayHeight>0?n.virtualDisplayHeight:.24,ipdFactor:1,parallaxFactor:1,perspectiveFactor:1}}function $n(n,e){let t=Number.isFinite(n.ipdFactor)?n.ipdFactor:1,i=Number.isFinite(n.parallaxFactor)?n.parallaxFactor:1;return{...n,ipdFactor:t*e,parallaxFactor:i*e}}var Un=16;function V(){return typeof performance<"u"&&performance&&typeof performance.now=="function"?performance.now():Date.now()}var Bn=1200,_i=!1;function qn(n){if(n==null)return ne;let e=dt(n);return e||(_i||(_i=!0,console.warn(`[inline3d] createInline3D({ modeSwitch: { easing: ${JSON.stringify(n)} } }) is not a curve this SDK knows (${ct.join(" / ")}); using '${ne}'.`)),ne)}var vi=!1;function Xn(){vi||(vi=!0,console.info("[inline3d] The active rendering mode is 1-view, so this SDK has zeroed every window rig (ipdFactor/parallaxFactor -> 0) \u2014 both eyes now render from one place. The runtime keeps weaving the same two-view atlas whatever the page submits, so leaving stereo in it would put two slightly different images on a flat panel, i.e. a blurry double image instead of 2D. Your rendering is unchanged; the flattening is a copy applied on the way to the layer and it is undone the moment a 2-view mode goes active again."))}function _t(n){return n&&n.detail!==void 0&&n.detail!==null?n.detail:void 0}function ki(n){let e=_t(n);return e!==void 0?[e,n]:[n]}function Gn(n){for(let e of ki(n)){if(typeof e=="number"&&Number.isFinite(e))return e;if(e&&typeof e=="object"){let t=e.modeIndex!==void 0?e.modeIndex:e.mode;if(typeof t=="number"&&Number.isFinite(t))return t}}return-1}function Jn(n){for(let e of ki(n)){if(e==="2d"||e==="3d")return e;if(e&&typeof e=="object"){let t=e.state!==void 0?e.state:e.hardwareDisplayState;if(t==="2d"||t==="3d")return t}}return null}var yi=!1;function Kn(){yi||(yi=!0,console.warn("[inline3d] This browser has no XRDisplayLayer.setViewRig, so the view rig was ignored (the window still weaves \u2014 the runtime keeps the default display rig, scaled by virtualDisplayHeight). Gate on inline3dViewRigSupported() if your page's framing depends on the rig; further calls are silent."))}var wi=!1;function Yn(){wi||(wi=!0,console.warn("[inline3d] addScene got BOTH viewRig and virtualDisplayHeight; on a browser with rig support the rig wins and the height is dropped. They describe the same slot \u2014 virtualDisplayHeight is shorthand for exactly one rig (display, identity pose, all factors 1) \u2014 so there is no merge. This is only worth passing as a PAIR deliberately: the height is what a browser without setViewRig will use, so a camera-rig scene can name its own fallback framing. Otherwise say it once, inside the rig."))}async function Ti(n={}){let{referenceSpace:e="viewer",lazy:t=!0,rootMargin:i="50% 0px",autoChrome:r=!0,modeSwitch:s=null,untrackedFallback:o="none",viewerEase:a}=n;if(!Ei())return{supported:!1,trackingState:"unknown"};let l;try{l=await navigator.xr.requestSession("inline-3d")}catch(p){return{supported:!1,trackingState:"unknown",error:p}}a!==void 0&&ri(l,a);let c=null;try{c=await l.requestReferenceSpace(e)}catch{}return new vt(l,c,{lazy:t,rootMargin:i,autoChrome:r,modeSwitch:s,untrackedFallback:o})}var we=null;function Fe(n={}){return U&&U._running?Promise.resolve(U):we||(we=Ti(n).finally(()=>{we=null}),we)}var bi=.7,Zn=new Set(["IMG","SVG","VIDEO","CANVAS","BUTTON","INPUT","SELECT","TEXTAREA"]);function Qn(n){let e=[],t=document.createTreeWalker(n,NodeFilter.SHOW_ELEMENT);for(let i=t.nextNode();i;i=t.nextNode())if(!i.closest("[data-inline3d-no-overlay]")){if(Zn.has(i.tagName.toUpperCase())){e.push(i);continue}for(let r of i.childNodes)if(r.nodeType===Node.TEXT_NODE&&r.nodeValue.trim()){e.push(i);break}}return e}var er=4;function xi(n){let e=typeof window<"u"&&window.devicePixelRatio||1;return`${n.clientWidth}x${n.clientHeight}@${e}`}var vt=class{constructor(e,t,{lazy:i,rootMargin:r,autoChrome:s=!0,modeSwitch:o=null,untrackedFallback:a="none"}){this.supported=!0,this.session=e,this.refSpace=t,this._windows=new Map,this._globalOverlays=new Set,this._isolatedBy=new WeakMap,this._autoChrome=s,this._autoChromeEls=new Set,this._lastChromeScan=0,this._fullTileWarned=new WeakSet,this._stoodDown=!1,this._activeModeIndex=-1,this._activeViewCount=0,this._hardwareDisplayState=null,this._modes=null,this._stereoCollapsed=!1,this._displayListeners=new Map,this._primedDisplayState=!1;let l=o&&typeof o=="object"?o:{};this._msEnabled=l.enabled!==!1,this._modeSwitch=new Ae((Number.isFinite(l.durationMs)?Math.max(0,l.durationMs):180)/1e3,qn(l.easing)),this._stereoFactor=1,this._msFire=null,this._msArmedUp=!1,this._msLastMs=null,this._msTick=null,this._trackingSupported=Ie(e)!==null,this._trackingState=this._trackingSupported?Ie(e):"unknown",this._untrackedFallback=a==="mono"?"mono":"none",this._bakedStereo=1,this._bakedTarget=1,this._bakedAt=null,this._bakedEaseS=(Number.isFinite(l.durationMs)?Math.max(0,l.durationMs):180)/1e3,this._applyUntrackedTarget(),this.undock=ut()?{model:!1,splat:!1}:null,this._undockRead=!1,this._running=!0,this._lazy=i,this._observer=i&&typeof IntersectionObserver=="function"?new IntersectionObserver(c=>this._onIntersect(c),{rootMargin:r}):null,this._loopId=0,this._framePending=!1,this._frameCount=0,this._frameWatchdog=null,this._suspended=null,U&&U._running&&U!==this&&console.warn("[inline3d] A second inline-3D session is live in this document. The browser's element-rect channel is a whole-widget setter, so both managers clobber each other's rect list every frame \u2014 tiles may flicker, ghost, or weave at a stale rect. Use ONE createInline3D() per document and add every window to it; if you are switching views, close() the previous manager first."),U=this,e.addEventListener("end",()=>this._teardown()),this._bindDisplayEvents(),this._bindTrackingEvents(),this._scanChrome(),this._bindLifecycle(),this._armDprWatch(),this._requestFrame()}get liveCount(){let e=0;for(let t of this._windows.values())t.layer&&e++;return e}addGlobalOverlay(e){if(!(!e||this._globalOverlays.has(e))){re()&&pt(),this._globalOverlays.add(e);for(let t of this._windows.values())t.layer&&this._applyExclusion(t,e)}}removeGlobalOverlay(e){if(!(!e||!this._globalOverlays.delete(e)))for(let t of this._windows.values())t.layer&&this._dropExclusion(t,e)}_scanChrome(){if(re()||!this._autoChrome||!Oe()||typeof document>"u")return;let e=Date.now();if(e-this._lastChromeScan<1e3)return;this._lastChromeScan=e;let t=document.body;if(!t)return;let i=new Set,r=t.querySelectorAll(":scope > *, :scope > * > *, :scope > * > * > *");for(let s of r){if(s.closest("[data-inline3d-no-overlay]"))continue;let o=getComputedStyle(s).position;if(o!=="fixed"&&o!=="sticky")continue;let a=!1;for(let l of this._windows.keys())if(s===l||s.contains(l)){a=!0;break}if(!a){i.add(s);for(let l of Qn(s))i.add(l)}}for(let s of this._autoChromeEls){let o=!1;if(s.isConnected){for(let a of this._windows.keys())if(s===a||s.contains(a)){o=!0;break}}(!s.isConnected||o)&&(this._autoChromeEls.delete(s),this.removeGlobalOverlay(s))}for(let s of i)this._globalOverlays.has(s)||(this._autoChromeEls.add(s),this.addGlobalOverlay(s))}addImage(e,t,i={}){let r=this._register(e,"image",i);return r.ready=ir(t).then(s=>{r.img=s,r.repaint()}),this._handle(e,r)}addVideo(e,t,i={}){let r=this._register(e,"video",i);return r.video=t,this._handle(e,r)}addScene(e,t,i={}){i.viewRig&&i.virtualDisplayHeight!==void 0&&Yn();let r=this._register(e,"scene",{virtualDisplayHeight:.24,...i});return r.onFrame=t,r.ownsBuffer=!1,i.bufferClamped===!0&&(r.warnedBufMismatch=!0),this._handle(e,r)}_handle(e,t){let i=this;return{remove:()=>this._remove(e),exclude:r=>{r&&(re()&&pt(),t.excluded.add(r),this._applyExclusion(t,r))},unexclude:r=>{!r||!t.excluded.delete(r)||this._dropExclusion(t,r)},get trackingState(){return i._trackingState},setViewRig:r=>(t.viewRig=r||null,this._pushViewRig(t)),getDisplayInfo:()=>this._layerCall(t,"getDisplayInfo","getDisplayInfo()"),displayMetrics:async()=>{let r=null;try{r=await this._layerCall(t,"getDisplayInfo","getDisplayInfo()")}catch{}return si(r,e.getBoundingClientRect(),globalThis.devicePixelRatio||1)},getRenderingModes:()=>this._getRenderingModes(t),requestRenderingMode:r=>this._requestRenderingModeEased(r,t),setStereoEnabled:r=>this._setStereoEnabled(r,t),on:(r,s)=>this.on(r,s),off:(r,s)=>this.off(r,s),onDisplayModeChange:r=>this._onDisplayModeChange(r),stats:()=>({frames:t.frames,monoFrames:t.monoFrames}),get firstWoven(){return t.fwPromise},onFirstWoven:r=>{if(typeof r!="function")throw new TypeError("[inline3d] onFirstWoven() takes a function.");let s=!0;return t.fwPromise.then(o=>{if(s)try{r(o)}catch(a){console.error("[inline3d] onFirstWoven callback threw",a)}}),()=>{s=!1}},rewoven:()=>this._rewoven(t)}}getDisplayInfo(){return this._layerCall(this._liveWindow(null),"getDisplayInfo","getDisplayInfo()")}getRenderingModes(){return this._getRenderingModes(null)}requestRenderingMode(e){return this._requestRenderingModeEased(e,null)}setStereoEnabled(e){return this._setStereoEnabled(e,null)}get trackingState(){return this._trackingState}get hardwareDisplayState(){return this._hardwareDisplayState}get activeMode(){return{modeIndex:this._activeModeIndex,viewCount:this._activeViewCount}}get stereoCollapsed(){return this._stereoCollapsed}get modeSwitch(){return{active:this._msTransitionActive(),factor:this._stereoFactor}}close(){try{this.session.end()}catch{}this._teardown()}_effectiveViewRig(e){return this._stereoFactor>=1?e.viewRig||(e.stereoSynthRig?mt(e):null):(e.viewRig||(e.stereoSynthRig=!0),$n(e.viewRig||mt(e),this._stereoFactor))}_setStereoCollapsed(e){let t=!!e;if(this._stereoCollapsed!==t){this._stereoCollapsed=t,t&&Xn(),!t&&this._msArmedUp?(this._msArmedUp=!1,this._startUpRamp()):this._modeSwitch.active()||(this._stereoFactor=t?0:1);for(let i of this._windows.values()){if(t){let r=i.viewRig||mt(i);i.stereoSaved={ipdFactor:r.ipdFactor,parallaxFactor:r.parallaxFactor}}else i.stereoSaved=null;this._pushViewRig(i)}}}_pushViewRig(e){if(!ht())return Kn(),!1;if(!e.layer)return!1;let t=this._effectiveViewRig(e);try{return e.layer.setViewRig(t),!0}catch{return!1}}_layerCall(e,t,i,r=[]){if(!ft())return Promise.reject(new Error(`[inline3d] ${i} needs a DisplayXR Browser with the display-mode API (XRDisplayLayer.getDisplayInfo/getRenderingModes/requestRenderingMode). Gate on inline3dDisplayModesSupported().`));if(!e||!e.layer)return Promise.reject(new Error(`[inline3d] ${i} needs a live weave layer, and this window has none right now (lazy mode closes the layer while the tile is off screen). Call it once the tile is visible, or create the manager with { lazy: false }.`));try{return Promise.resolve(e.layer[t](...r))}catch(s){return Promise.reject(s)}}_liveWindow(e){if(e&&e.layer)return e;for(let t of this._windows.values())if(t.layer)return t;return e||null}async _getRenderingModes(e){let t=await this._layerCall(this._liveWindow(e),"getRenderingModes","getRenderingModes()"),i=Array.isArray(t)?t:[];this._modes=i;let r=i.find(s=>s.isActive);return r&&(this._activeModeIndex=r.modeIndex,this._activeViewCount=r.viewCount,(r.viewCount===1||r.viewCount===2)&&this._setStereoCollapsed(r.viewCount===1)),this._primedDisplayState=!0,t}_requestRenderingMode(e,t){return this._layerCall(this._liveWindow(t),"requestRenderingMode","requestRenderingMode()",[e])}_msTransitionActive(){return this._modeSwitch.active()||this._msArmedUp||this._msFire!==null}async _requestRenderingModeEased(e,t){if(!this._msEnabled||!ht())return this._requestRenderingMode(e,t);let i=this._modes;if(!Array.isArray(i)||i.length===0)try{i=await this._getRenderingModes(t)}catch{i=null}let r=(Array.isArray(i)?i:[]).find(a=>a&&a.modeIndex===e),s=r&&Number.isFinite(r.viewCount)?r.viewCount:0,o=this._activeViewCount;return s<1||s>2||o<1?this._requestRenderingMode(e,t):s===1?o===1?this._requestRenderingMode(e,t):this._rampDownThenRequest(e,t):this._requestThenRampUp(e,t)}_rampDownThenRequest(e,t){if(this._msFire&&this._msFire.modeIndex===e)return this._msFire.promise;this._settlePendingDown("superseded",`a request for mode ${e} replaced it`);let i={modeIndex:e,win:t,resolve:null,reject:null,promise:null};return i.promise=new Promise((r,s)=>{i.resolve=r,i.reject=s}),this._msFire=i,this._modeSwitch.request({targetMode:e,targetViewCount:1,currentMode:this._activeModeIndex,currentViewCount:this._activeViewCount,current:this._stereoFactor,steady:1}),this._msLastMs=V(),this._armModeSwitchTick(),i.promise}_requestThenRampUp(e,t){let r=this._msFire!==null&&e===this._activeModeIndex;this._settlePendingDown("superseded",`a request for mode ${e} reversed it`);let s=r?Promise.resolve(void 0):this._requestRenderingMode(e,t);return this._stereoFactor<1&&(this._stereoCollapsed?(this._msArmedUp=!0,this._modeSwitch.cancel(),this._stereoFactor=0):this._startUpRamp()),s.catch(o=>{throw this._msArmedUp&&(this._msArmedUp=!1,this._modeSwitch.active()||(this._stereoFactor=this._stereoCollapsed?0:1)),o})}_startUpRamp(){this._settlePendingDown("superseded","the display returned to 3D"),this._modeSwitch.request({targetMode:this._activeModeIndex,targetViewCount:2,currentMode:this._activeModeIndex,currentViewCount:2,current:this._stereoFactor,steady:1}),this._msLastMs=V(),this._armModeSwitchTick()}_settlePendingDown(e,t){let i=this._msFire;if(!i)return;this._msFire=null;let r=new Error(`[inline3d] the request for rendering mode ${i.modeIndex} was never forwarded: ${t}. A ->2D switch is held until the disparity has ramped out, so a request that is reversed or replaced in that window is dropped rather than fired late.`);r.name=e,i.reject(r)}_advanceModeSwitch(){if(!this._modeSwitch.active()){this._msLastMs=null,this._disarmModeSwitchTick();return}let e=V(),t=typeof this._msLastMs=="number"?Math.max(0,(e-this._msLastMs)/1e3):0;this._msLastMs=e;let i=this._modeSwitch.update(t);if(i.factor!==this._stereoFactor){this._stereoFactor=i.factor;for(let r of this._windows.values())this._pushViewRig(r)}if(i.fire&&this._msFire){let r=this._msFire;this._msFire=null,this._requestRenderingMode(r.modeIndex,r.win).then(s=>r.resolve(s),s=>{this._startUpRamp(),r.reject(s)})}this._modeSwitch.active()||(this._msLastMs=null,this._disarmModeSwitchTick())}_armModeSwitchTick(){this._msTick!==null||typeof setTimeout!="function"||(this._msTick=setTimeout(()=>{this._msTick=null,this._running&&(this._advanceModeSwitch(),this._modeSwitch.active()&&this._armModeSwitchTick())},Un))}_disarmModeSwitchTick(){this._msTick!==null&&(typeof clearTimeout=="function"&&clearTimeout(this._msTick),this._msTick=null)}async _setStereoEnabled(e,t){let i=e?2:1,r=await this._getRenderingModes(t),s=Array.isArray(r)?r:[],o=s.find(a=>a.viewCount===i&&a.isRequestable);if(!o)throw new Error(`[inline3d] setStereoEnabled(${!!e}) found no requestable ${i}-view mode on this display (${s.length} mode(s) listed). It is sugar over requestRenderingMode() and cannot invent one \u2014 read getRenderingModes() and drive the list yourself.`);return o.isActive&&!this._msTransitionActive()||await this._requestRenderingModeEased(o.modeIndex,t),!!e}_bindDisplayEvents(){let e=this.session;!e||typeof e.addEventListener!="function"||ft()&&(e.addEventListener("renderingmodechange",t=>{this._onRenderingModeChange(t).catch(()=>{})}),e.addEventListener("hardwaredisplaystatechange",t=>{this._onHardwareDisplayStateChange(t).catch(()=>{})}))}_onHardwareDisplayStateChange(e){let t=Jn(e),i=_t(e),r=a=>{a&&(this._hardwareDisplayState=a),this._emitDisplay({type:"hardwaredisplaystatechange",state:a||this._hardwareDisplayState,detail:i})};t&&r(t);let s=this._activeModeIndex,o=this._activeViewCount;return this._getRenderingModes(null).then(a=>{let l=(Array.isArray(a)?a:[]).find(c=>c.isActive)||null;if(l&&(l.modeIndex!==s||l.viewCount!==o)&&this._emitDisplay({type:"renderingmodechange",modeIndex:this._activeModeIndex,viewCount:this._activeViewCount||null,mode:l,detail:null}),!t){let c=l&&typeof l.hardwareDisplay3D=="boolean"?l.hardwareDisplay3D?"3d":"2d":null;r(c)}}).catch(()=>{t||r(null)})}async _onRenderingModeChange(e){let t=_t(e),i=Gn(e),r=null;try{let s=await this._getRenderingModes(null),o=Array.isArray(s)?s:[];r=(i>=0?o.find(a=>a.modeIndex===i):null)||o.find(a=>a.isActive)||null}catch{}r?(this._activeModeIndex=r.modeIndex,this._activeViewCount=r.viewCount):i>=0&&(this._activeModeIndex=i),(this._activeViewCount===1||this._activeViewCount===2)&&this._setStereoCollapsed(this._activeViewCount===1),this._emitDisplay({type:"renderingmodechange",modeIndex:this._activeModeIndex,viewCount:this._activeViewCount||null,mode:r,detail:t})}_emitDisplay(e,t){let i=this._displayListeners.get(e.type);if(!i)return;let r=t||[e];for(let s of[...i])try{s(...r)}catch(o){console.error(`[inline3d] ${e.type} listener threw`,o)}}on(e,t){if(typeof t!="function")throw new TypeError("[inline3d] on() takes (type, function).");if(!mi.includes(e))throw new TypeError(`[inline3d] on() knows ${mi.join(" / ")}, got ${JSON.stringify(e)}.`);let i=this._displayListeners.get(e);return i||this._displayListeners.set(e,i=new Set),i.add(t),()=>this.off(e,t)}off(e,t){let i=this._displayListeners.get(e);i&&i.delete(t)}_onDisplayModeChange(e){if(typeof e!="function")throw new TypeError("[inline3d] onDisplayModeChange() takes a function.");let t=Si.map(i=>this.on(i,e));return()=>{for(let i of t)i()}}async refreshUndock(){if(!ut())return this.undock=null;let e=this._liveWindow(null);if(!e||!e.layer||typeof e.layer.getUndockCapabilities!="function")return this.undock;try{let t=await e.layer.getUndockCapabilities();this.undock={model:!!(t&&t.model),splat:!!(t&&t.splat)}}catch{}return this.undock}_register(e,t,i){this._windows.has(e)&&this._remove(e),e.style.willChange="transform",e.style.transform="translateZ(0)";let r={canvas:e,kind:t,layer:null,img:null,video:null,onFrame:null,onLayerLost:typeof i.onLayerLost=="function"?i.onLayerLost:null,layerLostSent:!1,ready:null,ownsBuffer:t!=="scene",cornerRadius:i.cornerRadius||0,feather:i.feather||0,reqW:i.width||0,reqH:i.height||0,virtualDisplayHeight:i.virtualDisplayHeight||0,viewRig:i.viewRig||null,stereoSaved:null,stereoSynthRig:!1,observeEl:i.observe||e,ctx:t==="scene"?null:e.getContext("2d"),repaint:()=>this._paint(r,null),excluded:new Set,autoExcluded:new Set,overlayObserver:null,sizeObserver:null,resizePending:!1,frames:0,monoFrames:0,fwResult:null,fwResolve:null,fwPromise:null,fwHoldMs:tr(i.firstWovenHoldMs),fwRegAt:V(),fwLayerAt:null,fwStereo:!1,rw:null,rwGone:null};return r.fwPromise=new Promise(s=>{r.fwResolve=s}),this._windows.set(e,r),this._lazy&&this._observer?this._observer.observe(r.observeEl):this._activate(r),r}_remove(e){let t=this._windows.get(e);t&&(this._observer&&this._observer.unobserve(t.observeEl),this._deactivate(t),this._windows.delete(e),this._settleFirstWoven(t,!1,"removed"))}_onIntersect(e){for(let t of e){let i=null;for(let r of this._windows.values())if(r.observeEl===t.target){i=r;break}i&&(t.isIntersecting?this._activate(i):this._deactivate(i))}}_activate(e){if(!e.layer){this._scanChrome();try{let t=ht()?this._effectiveViewRig(e):null,i=t?{viewRig:t}:e.virtualDisplayHeight>0?{virtualDisplayHeight:e.virtualDisplayHeight}:{};e.layer=new XRDisplayLayer(this.session,e.canvas,i)}catch(t){e.layer=null,e.layerFailWarned||(e.layerFailWarned=!0,console.warn("[inline3d] new XRDisplayLayer() failed for this window \u2014 it will show FLAT 2D instead of woven 3D, and the SDK will not retry. The canvas has been repainted mono so it cannot be left holding a raw side-by-side pair.",t)),this._paintMono(e),this._notifyLayerLost(e),this._settleFirstWoven(e,!1,"layer-failed");return}e.fwResult||(e.fwLayerAt=V(),e.fwStereo=!1),this._restartRewoven(e),e.layerLostSent=!1,!this._primedDisplayState&&ft()&&(this._primedDisplayState=!0,this._getRenderingModes(e).catch(()=>{this._primedDisplayState=!1})),ut()&&this.undock&&!this._undockRead&&(this._undockRead=!0,this.refreshUndock().catch(()=>{})),Vn(e.layer)===!0&&this._standDownLegacyOcclusion();for(let t of e.excluded)this._applyExclusion(e,t);for(let t of this._globalOverlays)this._applyExclusion(e,t);this._startOverlayScan(e),e.ownsBuffer&&(this._sizeBuffer(e,!0),this._paint(e,null)),this._startSizeWatch(e)}}_deactivate(e){if(this._stopOverlayScan(e),this._stopSizeWatch(e),e.layer){try{e.layer.close()}catch{}e.layer=null}this._paintMono(e)}_paintMono(e){!e.ownsBuffer||e.kind==="scene"||(this._sizeBuffer(e,!1),this._paint(e,null))}_notifyLayerLost(e){if(!(e.kind!=="scene"||typeof e.onLayerLost!="function"||e.layerLostSent)){e.layerLostSent=!0;try{e.onLayerLost()}catch(t){console.warn("[inline3d] a scene window's onLayerLost callback threw",t)}}}_isFullTileOverlay(e,t){if(!t||typeof t.getBoundingClientRect!="function")return!1;let i=t.getBoundingClientRect(),r=e.canvas.getBoundingClientRect(),s=i.width*i.height,o=r.width*r.height;if(s<=0||o<=0)return!1;let a=Math.min(i.right,r.right)-Math.max(i.left,r.left),l=Math.min(i.bottom,r.bottom)-Math.max(i.top,r.top);if(a<=0||l<=0)return!1;let c=a*l;return c/s<bi||c/o<bi?!1:(this._fullTileWarned.has(t)||(this._fullTileWarned.add(t),console.warn("[inline3d] Refusing a full-tile overlay: this element covers its own woven canvas, and the browser's geometric matcher cannot tell the two apart \u2014 it would stage the CANVAS as the overlay and the tile would show its raw side-by-side buffer instead of 3D. Make the overlay a PARTIAL region of the tile (a caption band, a badge, a corner plate), or move it outside the tile and register it with addGlobalOverlay().",t)),!0)}_applyExclusion(e,t){if(re()||!e.layer||!Oe()||this._isFullTileOverlay(e,t))return;let i=this._isolatedBy.get(t);if(i||(i=new Set,this._isolatedBy.set(t,i)),i.add(e),!t.dataset.inline3dIsolated){t.dataset.inline3dPriorWillChange=t.style.willChange||"";let r=t.style.willChange&&t.style.willChange!=="auto"?t.style.willChange+", transform":"transform";t.style.willChange=r,t.dataset.inline3dIsolated="1"}try{e.layer.excludeElement(t)}catch{}}_dropExclusion(e,t){if(re())return;let i=this._isolatedBy.get(t);if(i&&i.delete(e),(!i||i.size===0)&&this._unpromote(t),!(!e.layer||!Oe()))try{e.layer.unexcludeElement(t)}catch{}}_unpromote(e){return!e||!e.dataset||!e.dataset.inline3dIsolated?!1:(e.style.willChange=e.dataset.inline3dPriorWillChange||"",delete e.dataset.inline3dPriorWillChange,delete e.dataset.inline3dIsolated,!0)}_standDownLegacyOcclusion(){if(this._stoodDown)return;this._stoodDown=!0;let e=!1;for(let t of this._windows.values()){t.overlayObserver&&(this._stopOverlayScan(t),e=!0);for(let i of t.excluded)this._unpromote(i)&&(e=!0)}for(let t of this._autoChromeEls)this._globalOverlays.delete(t),this._unpromote(t),e=!0;this._autoChromeEls.clear();for(let t of this._globalOverlays)this._unpromote(t)&&(e=!0);this._isolatedBy=new WeakMap,e&&pt()}_startOverlayScan(e){if(re()||!Oe()||typeof MutationObserver!="function")return;let t=e.canvas.parentElement;if(!t)return;let i=()=>{let r=new Set(t.querySelectorAll("[data-inline3d-overlay]"));for(let s of e.autoExcluded)r.has(s)||(e.autoExcluded.delete(s),this._dropExclusion(e,s));for(let s of r)e.autoExcluded.has(s)||(e.autoExcluded.add(s),this._applyExclusion(e,s))};i(),e.overlayObserver=new MutationObserver(i),e.overlayObserver.observe(t,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["data-inline3d-overlay"]})}_stopOverlayScan(e){e.overlayObserver&&(e.overlayObserver.disconnect(),e.overlayObserver=null),e.autoExcluded.clear()}_eyeSize(e,t=!0){let i=Math.min(window.devicePixelRatio||1,2),r=e.reqW||Math.round((e.canvas.clientWidth||256)*i),s=e.reqH||Math.round((e.canvas.clientHeight||256)*i),o=1,a=!e.reqW&&!e.reqH&&this._sourceEyeSize(e);if(a){let c=Math.max(a.w/r,a.h/s);c<1&&(o=c,r=Math.max(1,Math.round(r*c)),s=Math.max(1,Math.round(s*c)))}let l=ci(r,s,li(),{cols:t?2:1});return l.clamped&&(o*=l.scale,r=l.eyeW,s=l.eyeH,e.warnedBufClamp||(e.warnedBufClamp=!0,console.warn(di("[inline3d]",l)))),e.bufScale=o,{w:r,h:s}}_sourceEyeSize(e){if(e.kind!=="video"&&e.kind!=="image")return null;let t=e.kind==="video"?e.video:e.img;if(!t)return null;let i=t.videoWidth||t.naturalWidth||t.width,r=t.videoHeight||t.naturalHeight||t.height;return!(i>1)||!(r>0)?null:{w:i/2,h:r}}_checkSceneBuffer(e){let t=e.canvas;if(!t||typeof t.getContext!="function")return;if(e.sceneGl===void 0){let r=null;try{r=t.getContext("webgl2")||t.getContext("webgl")}catch{r=null}e.sceneGl=r&&typeof r.drawingBufferWidth=="number"?r:null}if(!e.sceneGl)return;let i=hi(t,e.sceneGl);i.mismatch&&(e.warnedBufMismatch=!0,console.warn(fi("[inline3d]",t,i,"getViewport() splits canvas.width, so the eyes are off-centre in the woven buffer. This scene canvas is sized by the page: keep 2 \xD7 eye width and the height within MAX_TEXTURE_SIZE / MAX_RENDERBUFFER_SIZE / MAX_VIEWPORT_DIMS, or map getViewport() onto gl.drawingBufferWidth/Height.")))}_sizeBuffer(e,t){let{w:i,h:r}=this._eyeSize(e,t);e.eyeW=i,e.eyeH=r,e.canvas.width=t?i*2:i,e.canvas.height=r,e.sbs=t}_startSizeWatch(e){typeof ResizeObserver=="function"&&(e.sizeObserver||!e.ownsBuffer||e.reqW&&e.reqH||(e.sizeObserver=new ResizeObserver(()=>this._onBoxChange(e)),e.sizeObserver.observe(e.canvas)))}_stopSizeWatch(e){e.sizeObserver&&(e.sizeObserver.disconnect(),e.sizeObserver=null),e.resizePending=!1}_onBoxChange(e){if(!e.layer||!e.ownsBuffer||e.resizePending)return;e.resizePending=!0;let t=()=>{if(!e.resizePending||(e.resizePending=!1,!e.layer||!e.ownsBuffer))return;this._noteRewovenBox(e);let{w:i,h:r}=this._eyeSize(e);i===e.eyeW&&r===e.eyeH||(this._sizeBuffer(e,!0),this._paint(e,null))};typeof requestAnimationFrame=="function"?requestAnimationFrame(t):t()}_armDprWatch(){if(typeof window>"u"||typeof window.matchMedia!="function")return;this._disarmDprWatch();let e;try{e=window.matchMedia(`(resolution: ${window.devicePixelRatio||1}dppx)`)}catch{return}let t=()=>{if(this._running){this._armDprWatch();for(let i of this._windows.values())i.layer&&this._onBoxChange(i)}};try{e.addEventListener("change",t)}catch{return}this._dprWatch={q:e,onChange:t}}_disarmDprWatch(){if(this._dprWatch){try{this._dprWatch.q.removeEventListener("change",this._dprWatch.onChange)}catch{}this._dprWatch=null}}_paint(e,t){if(e.kind==="scene"||!e.ctx)return;let i=this._running&&!!e.layer;!i&&e.sbs&&this._sizeBuffer(e,!1);let r=e.kind==="video"?e.video:e.img;if(!r)return;if(e.kind==="video"&&(r.readyState||0)<2){i&&this._recommitLastFrame(e);return}let s=e.canvas,o=e.ctx,a=r.videoWidth||r.naturalWidth||r.width,l=r.videoHeight||r.naturalHeight||r.height;if(!a||!l)return;let c=a+"x"+l;if(e.srcKey!==c&&(e.srcKey=c,e.ownsBuffer&&!(e.reqW&&e.reqH))){let{w:u,h:v}=this._eyeSize(e,e.sbs);(u!==e.eyeW||v!==e.eyeH)&&this._sizeBuffer(e,e.sbs)}let p=e.bufScale||1,f=e.cornerRadius*p,h=e.feather*p;if(o.clearRect(0,0,s.width,s.height),!e.sbs){Pe(o,r,0,0,a/2,l,0,0,s.width,s.height,f,h);return}let d=s.width/2,m=Math.min(this._stereoFactor,this._bakedStereo);if(m<1){Pe(o,r,0,0,a/2,l,0,0,d,s.height,f,h),nr(o,r,a/2,l,d,s.height,m,f,h);return}f>0||h>0?(Pe(o,r,0,0,a/2,l,0,0,d,s.height,f,h),Pe(o,r,a/2,0,a/2,l,d,0,d,s.height,f,h)):o.drawImage(r,0,0,a,l,0,0,s.width,s.height)}_bindTrackingEvents(){let e=this.session;!e||typeof e.addEventListener!="function"||this._trackingSupported&&e.addEventListener(gt,()=>{this._setTrackingState(Ie(e)||"unknown")})}_setTrackingState(e){let t=Mi.includes(e)?e:"unknown";if(t===this._trackingState)return;this._trackingState=t,this._applyUntrackedTarget();let i={type:gt,state:t};this._emitDisplay(i,[t,i])}_applyUntrackedTarget(){if(this._untrackedFallback!=="mono"){this._bakedTarget=1;return}this._trackingState==="searching"?this._bakedTarget=0:this._trackingState==="tracking"&&(this._bakedTarget=1)}_trackBakedStereo(e){this._trackingSupported&&this._setTrackingState(Ie(this.session)||"unknown");let t=this._bakedAt===null||!Number.isFinite(e)?0:Math.max(0,(e-this._bakedAt)/1e3);this._bakedAt=Number.isFinite(e)?e:this._bakedAt;let i=this._bakedStereo,r=this._bakedTarget;if(i===r)return;let s=this._bakedEaseS>0?t/this._bakedEaseS:1;this._bakedStereo=r>i?Math.min(r,i+s):Math.max(r,i-s)}_recommitLastFrame(e){let t=e.canvas;if(!t.width||!t.height)return;let i=e.ctx;try{i.save(),i.globalCompositeOperation="copy",i.drawImage(t,0,0),i.restore()}catch{}}_requestFrame(e){if(!this._running||this._framePending&&!e)return;let t=e?++this._loopId:this._loopId;this._framePending=!0;try{this.session.requestAnimationFrame((i,r)=>{t===this._loopId&&(this._framePending=!1,this._frameCount++,this._frame(i,r))})}catch{this._framePending=!1}}_frame(e,t){if(!this._running)return;this._requestFrame(),this._advanceModeSwitch();let i=this.refSpace?t.getViewerPose(this.refSpace):null,r=i?i.views:null;this._trackBakedStereo(e);for(let s of this._windows.values())if(s.layer){if(s.kind==="scene"){if(r&&s.onFrame){if(s.frames++,r.length<2&&(s.monoFrames++,s.monoFrames%300===1&&typeof console<"u"&&console.debug)){let o=(100*s.monoFrames/Math.max(1,s.frames)).toFixed(1);console.debug(`[inline3d] scene window: ${s.monoFrames} non-stereo view lists in ${s.frames} frames (${o}%). The viewer replays its last good stereo frame for these; a rising rate means the session is falling back under load.`)}try{s.onFrame(r,s.layer,t),r.length>=2&&(s.fwStereo=!0,s.warnedBufMismatch||this._checkSceneBuffer(s),s.rw&&(s.rw.stereo=!0))}catch(o){s.frameThrewWarned||(s.frameThrewWarned=!0,console.warn("[inline3d] a scene window's onFrame threw; this window will keep whatever it last drew, and the other windows carry on. Further throws from it are silent.",o))}}}else this._paint(s,r),s.sbs&&(s.kind==="video"?(s.video&&s.video.readyState||0)>=2:s.img)&&(s.fwStereo=!0,s.rw&&(s.rw.stereo=!0));this._tickFirstWoven(s)}}_tickFirstWoven(e){if(e.rw){this._noteRewovenBox(e);let t=e.rw,i=V();t.stereo&&i-t.at>=e.fwHoldMs?this._settleRewoven(e,!0,"hold-elapsed"):t.at>t.calledAt&&i-t.calledAt>=er*e.fwHoldMs&&this._settleRewoven(e,!0,"hold-capped")}e.fwResult||!e.fwStereo||e.fwLayerAt===null||V()-e.fwLayerAt<e.fwHoldMs||this._settleFirstWoven(e,!0,"hold-elapsed")}_settleFirstWoven(e,t,i){t||(e.rwGone||(e.rwGone=i),this._settleRewoven(e,!1,i)),!e.fwResult&&(e.fwResult=Object.freeze({woven:t,confirmed:!1,reason:i,ms:Math.round(V()-e.fwRegAt)}),e.fwResolve(e.fwResult),e.fwResolve=null)}_rewoven(e){if(!e.fwResult)return e.fwPromise;if(!e.fwResult.woven)return Promise.resolve(e.fwResult);if(e.rwGone)return Promise.resolve(Object.freeze({woven:!1,confirmed:!1,reason:e.rwGone,ms:0}));if(e.rw)return this._restartRewoven(e),e.rw.promise;let t,i=new Promise(s=>{t=s}),r=V();return e.rw={promise:i,resolve:t,calledAt:r,at:r,stereo:!1,box:xi(e.canvas)},i}_noteRewovenBox(e){let t=e.rw;if(!t)return;let i=xi(e.canvas);i!==t.box&&(t.box=i,this._restartRewoven(e))}_restartRewoven(e){e.rw&&(e.rw.at=V(),e.rw.stereo=!1)}_settleRewoven(e,t,i){let r=e.rw;r&&(e.rw=null,r.resolve(Object.freeze({woven:t,confirmed:!1,reason:i,ms:Math.round(V()-r.calledAt)})))}_bindLifecycle(){typeof window>"u"||(this._onPageHide=()=>this._suspend(),this._onPageShow=e=>this._resume(!!(e&&e.persisted)),window.addEventListener("pagehide",this._onPageHide),window.addEventListener("pageshow",this._onPageShow),typeof document<"u"&&"onfreeze"in document&&(this._onFreeze=()=>this._suspend(),this._onResume=()=>this._resume(!0),document.addEventListener("freeze",this._onFreeze),document.addEventListener("resume",this._onResume)))}_unbindLifecycle(){typeof window>"u"||(this._onPageHide&&window.removeEventListener("pagehide",this._onPageHide),this._onPageShow&&window.removeEventListener("pageshow",this._onPageShow),this._onFreeze&&typeof document<"u"&&(document.removeEventListener("freeze",this._onFreeze),document.removeEventListener("resume",this._onResume)),this._onPageHide=this._onPageShow=this._onFreeze=this._onResume=null,this._frameWatchdog&&(clearTimeout(this._frameWatchdog),this._frameWatchdog=null))}_suspend(){if(!this._running||this._suspended)return;let e=[];for(let t of this._windows.values())t.layer&&(e.push(t),this._deactivate(t));this._suspended=e}_resume(e){if(!this._running)return;let t=this._suspended;if(this._suspended=null,t)for(let i of t)this._windows.has(i.canvas)&&(this._lazy&&this._observer?(this._observer.unobserve(i.observeEl),this._observer.observe(i.observeEl)):this._activate(i));this._lastChromeScan=0,this._scanChrome(),this._armDprWatch(),e&&this._watchForStalledFrames()}_watchForStalledFrames(){if(this._frameWatchdog||typeof setTimeout!="function")return;let e=this._frameCount;this._frameWatchdog=setTimeout(()=>{this._frameWatchdog=null,!(!this._running||this._frameCount!==e)&&this._requestFrame(!0)},1e3)}_teardown(){if(this._running){this._running=!1,U===this&&(U=null),this._unbindLifecycle(),this._disarmDprWatch(),this._disarmModeSwitchTick(),this._modeSwitch.cancel(),this._msArmedUp=!1,this._settlePendingDown("closed","the inline-3D session closed first"),this._observer&&this._observer.disconnect();for(let e of this._windows.values()){if(this._stopOverlayScan(e),this._stopSizeWatch(e),e.layer){try{e.layer.close()}catch{}e.layer=null}this._paintMono(e),this._notifyLayerLost(e),this._settleFirstWoven(e,!1,"session-ended")}this._windows.clear(),this._setTrackingState("unknown"),this._displayListeners.clear()}}};function tr(n){return typeof n=="number"&&Number.isFinite(n)&&n>=0?n:Bn}function ir(n){return typeof n!="string"?Promise.resolve(n):new Promise((e,t)=>{let i=new Image;i.decoding="async",i.onload=()=>e(i),i.onerror=t,i.src=n})}function Pe(n,e,t,i,r,s,o,a,l,c,p,f){p>0&&n.roundRect?(n.save(),n.beginPath(),n.roundRect(o,a,l,c,p),n.clip(),n.drawImage(e,t,i,r,s,o,a,l,c),n.restore()):n.drawImage(e,t,i,r,s,o,a,l,c),f>0&&Ci(n,o,a,l,c,f)}function nr(n,e,t,i,r,s,o,a,l){n.save(),a>0&&n.roundRect&&(n.beginPath(),n.roundRect(r,0,r,s,a),n.clip()),n.drawImage(e,0,0,t,i,r,0,r,s),o>0&&(n.globalAlpha=o,n.drawImage(e,t,0,t,i,r,0,r,s)),n.restore(),l>0&&Ci(n,r,0,r,s,l)}function Ci(n,e,t,i,r,s){let o=Math.min(s,Math.floor(Math.min(i,r)/2));if(o<=0)return;n.save(),n.globalCompositeOperation="destination-out";let a=[[e,t,i,o,[e,t],[e,t+o]],[e,t+r-o,i,o,[e,t+r],[e,t+r-o]],[e,t,o,r,[e,t],[e+o,t]],[e+i-o,t,o,r,[e+i,t],[e+i-o,t]]];for(let[l,c,p,f,h,d]of a){let m=n.createLinearGradient(h[0],h[1],d[0],d[1]);m.addColorStop(0,"rgba(0,0,0,1)"),m.addColorStop(1,"rgba(0,0,0,0)"),n.fillStyle=m,n.fillRect(l,c,p,f)}n.restore()}function oe(n,e){return n>0&&e>0&&n/e>2.5}function X(n,e,t,i,r){let s=Math.abs(i||0),o=Math.min(n-2*s,e*t);o=Math.max(1,o);let a=Math.min(e,o/t);o=a*t;let l=(n-o)/2+(r===0?i:-i);return{sx:Math.max(0,Math.min(n-o,l)),sy:(e-a)/2,sw:o,sh:a}}function be(n,e,t){let i=Math.min(n,e*t),r=i/t;return{w:Math.max(2,Math.round(i/2)*2),h:Math.max(2,Math.round(r/2)*2)}}function ae(n,e){let t=n/2;return[{src:"R",sx:t,sw:t,sy:0,sh:e,dx:0,dw:t,mirror:!0},{src:"L",sx:0,sw:t,sy:0,sh:e,dx:t,dw:t,mirror:!0}]}function yt(n,e,t){let i=new n.constructor(n.length);for(let r of ae(e,t))for(let s=0;s<t;s++)for(let o=0;o<r.dw;o++){let a=r.sx+(r.sw-1-o);i[s*e+r.dx+o]=n[s*e+a]}return i}function Ri(n,e){return!(n>0)||!(e>0)||e>=180?null:n/2/Math.tan(e/2*Math.PI/180)}function je({eyeWidthPx:n,hfovDeg:e,baselineMm:t,subjectZmm:i}){let r=Ri(n,e);return r===null||!(t>0)||!(i>0)?0:r*t/(2*i)}function We(n,e,t=.12){let i=Math.max(0,e*t);return Math.max(-i,Math.min(i,n||0))}function Ne(n,e,t=.2){let i=n+t*(e-n);return Math.abs(e-i)<.05?e:i}function xe(){let n={hello:null,subjectZmm:null,measuredPx:null,depth:0,current:0,target(e){let t=n.measuredPx!==null?n.measuredPx/2:n.hello&&n.subjectZmm?je({eyeWidthPx:e,hfovDeg:n.hello.hfovDeg,baselineMm:n.hello.baselineMm,subjectZmm:n.subjectZmm}):0;return We(t+n.depth*.05*e,e)},step(e){return n.current=Ne(n.current,n.target(e)),n.current}};return n}var ze=1,wt="inline3d-call/1",Di=Object.freeze(["sbs","mono"]),le=4,Li=4,bt=5,rr=150,sr=5e3,or=/^[A-Za-z0-9_-]{16,64}$/,ar=16;function xt(n){let e=new Uint8Array(ar);return(n||(i=>globalThis.crypto.getRandomValues(i)))(e),Oi(e)}function Ai(n){let e=new Uint8Array(8);return(n||(i=>globalThis.crypto.getRandomValues(i)))(e),Oi(e)}function Oi(n){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",t="",i=0;for(;i+2<n.length;i+=3){let s=n[i]<<16|n[i+1]<<8|n[i+2];t+=e[s>>18&63]+e[s>>12&63]+e[s>>6&63]+e[s&63]}let r=n.length-i;if(r===1){let s=n[i]<<16;t+=e[s>>18&63]+e[s>>12&63]}else if(r===2){let s=n[i]<<16|n[i+1]<<8;t+=e[s>>18&63]+e[s>>12&63]+e[s>>6&63]}return t}function ce(n){return typeof n=="string"&&or.test(n)}function Se(n){if(!n)return null;let e="";if(typeof n=="object")e=n.hash||n.href&&String(n.href).split("#")[1]||"";else{let t=String(n),i=t.indexOf("#");e=i>=0?t.slice(i+1):t.includes("=")&&!t.includes("://")?t:""}e=e.replace(/^#/,"");for(let t of e.split("&")){let i=t.indexOf("=");if(i<0||decodeURIComponent(t.slice(0,i))!=="room")continue;let r=decodeURIComponent(t.slice(i+1));return ce(r)?r:null}return null}function He(n,e){if(!ce(e))throw new Error(`@displayxr/inline3d/call: invalid room id "${e}"`);return`${String(n||"").split("#")[0]}#room=${e}`}function St(n){if(!n||typeof n!="object"||n.type!=="hello")return null;let e=(t,i,r)=>typeof t=="number"&&Number.isFinite(t)&&t>=i&&t<=r?t:null;return{type:"hello",v:typeof n.v=="number"?n.v:ze,format:Di.includes(n.format)?n.format:"mono",width:e(n.width,1,16384),height:e(n.height,1,16384),baselineMm:e(n.baselineMm,1,1e3),hfovDeg:e(n.hfovDeg,5,170),rectified:n.rectified===!0,sdk:typeof n.sdk=="string"?n.sdk.slice(0,64):null}}function Mt({format:n,width:e,height:t,baselineMm:i,hfovDeg:r,rectified:s}={}){let o={type:"hello",v:ze,format:Di.includes(n)?n:"mono",width:e||0,height:t||0,rectified:!!s,sdk:wt};return Number.isFinite(i)&&(o.baselineMm=i),Number.isFinite(r)&&(o.hfovDeg=r),o}function Et(n){if(!n||n.type!=="hint")return null;let e=n.subjectZmm;return typeof e=="number"&&Number.isFinite(e)&&e>=rr&&e<=sr?{type:"hint",subjectZmm:e}:null}function Ii(n){return!n||n.type!=="state"?null:{type:"state",muted:n.muted===!0,cameraOff:n.cameraOff===!0,speaking:n.speaking===!0}}function kt(n){let e=1e3/n,t=-1/0;return i=>i-t<e?!1:(t=i,!0)}function Tt({format:n,woven:e,mono3D:t="auto",lift:i=!1,overBudget:r=!1,failed:s=!1}){return(n==="sbs"?"sbs":"mono")==="sbs"?{route:e?"woven-sbs":"flat-left"}:e?t==="off"||t===!1?{route:"flat",mono3d:"off"}:i?s?{route:"flat",mono3d:"failed"}:r?{route:"flat",mono3d:"budget"}:{route:"lifted",mono3d:"lifted"}:{route:"flat",mono3d:"unavailable"}:{route:"flat"}}function Ct(n){return n==="woven-sbs"?"3D":n==="lifted"?"2D\u21923D":"2D"}function Rt({need:n=10,needNoPose:e=30}={}){let t=0,i=0,r=!1;return{feed(s){return r?!0:(i++,s===null?r=i>=e:(t=s>=1?t+1:0,r=t>=n),r)},get live(){return r}}}function Ve(n,e){let t=Math.max(1,e|0),r=n==="sbs"?[6e3,4e3,3e3]:[2500,1800,1400];return r[Math.min(t,r.length)-1]}function B(n,{baseMs:e=1e3,maxMs:t=3e4,rand:i=Math.random}={}){let r=Math.min(t,e*Math.pow(2,Math.max(0,n)));return Math.round(r*(.8+.4*i()))}var Me=Object.freeze(["VP9","VP8","AV1"]);function cr(n,e=Me){let t=e.indexOf(String(n||"").toUpperCase());return t<0?e.length:t}function $e(n,e=Me){let t=i=>cr(String(i.mimeType||"").split("/")[1],e);return n.map((i,r)=>({c:i,i:r,r:t(i)})).sort((i,r)=>i.r-r.r||i.i-r.i).map(i=>i.c)}function Ee(n,e=Me){if(!n)return n;let t=n.includes(`\r
|
|
3
3
|
`)?`\r
|
|
4
4
|
`:`
|
|
@@ -15,14 +15,16 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Placement: the same maths as everywhere else (./inline3d-cursor-depth.js, a port of the
|
|
17
17
|
// runtime's u_cursor_depth, ADR-046), solved in WORLD space from this frame's views carried
|
|
18
|
-
// through the rig node. Drawn as
|
|
19
|
-
// draws it and nothing occludes it
|
|
18
|
+
// through the rig node. Drawn as a filled mesh (strokes + a dark outline) in the UI layer with
|
|
19
|
+
// depth test off, so every eye camera draws it last and nothing occludes it; the picker skips the
|
|
20
|
+
// UI layer, so the cursor never picks itself.
|
|
20
21
|
|
|
21
22
|
import {
|
|
22
23
|
CursorDepthPlacer,
|
|
23
24
|
CursorPointer,
|
|
24
25
|
CURSOR_DEFAULT_HEIGHT,
|
|
25
|
-
|
|
26
|
+
CURSOR_DEFAULT_TUNING,
|
|
27
|
+
cursorCrosshairMesh,
|
|
26
28
|
cursorFootprint,
|
|
27
29
|
cursorViewRay,
|
|
28
30
|
} from './inline3d-cursor-depth.js';
|
|
@@ -38,15 +40,15 @@ export class PlayCanvasDepthCursor {
|
|
|
38
40
|
/**
|
|
39
41
|
* @param {object} pc the PlayCanvas namespace the adapter renders with.
|
|
40
42
|
* @param {object} viewer the PlayCanvasSplatViewer (app, rigNode, eye are read lazily).
|
|
41
|
-
* @param {{canvas: HTMLElement, height?: number,
|
|
43
|
+
* @param {{canvas: HTMLElement, height?: number, margin?: number, anchor?: string, pointerScope?: string}} opts
|
|
42
44
|
*/
|
|
43
|
-
constructor(pc, viewer, { canvas, height = CURSOR_DEFAULT_HEIGHT,
|
|
45
|
+
constructor(pc, viewer, { canvas, height = CURSOR_DEFAULT_HEIGHT, margin, anchor = 'hybrid', pointerScope = 'canvas' }) {
|
|
44
46
|
this.pc = pc;
|
|
45
47
|
this.viewer = viewer;
|
|
46
|
-
this.height = height;
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
this.placer = new CursorDepthPlacer();
|
|
48
|
+
this.height = height > 0 && Number.isFinite(height) ? height : CURSOR_DEFAULT_HEIGHT;
|
|
49
|
+
this.pointer = new CursorPointer(canvas, { scope: pointerScope });
|
|
50
|
+
const tuning = margin > 0 && Number.isFinite(margin) ? { ...CURSOR_DEFAULT_TUNING, margin } : CURSOR_DEFAULT_TUNING;
|
|
51
|
+
this.placer = new CursorDepthPlacer(tuning, { anchor });
|
|
50
52
|
/** The last placement (diagnostics). */
|
|
51
53
|
this.placement = { active: false };
|
|
52
54
|
/** The last picked world point under the footprint, or null (nothing under it). */
|
|
@@ -58,8 +60,7 @@ export class PlayCanvasDepthCursor {
|
|
|
58
60
|
this._pickStart = 0;
|
|
59
61
|
this._frame = 0;
|
|
60
62
|
this._disposed = false;
|
|
61
|
-
this.
|
|
62
|
-
this._colors = [];
|
|
63
|
+
this._sprite = null; // { mi, node, mat } — built on first use
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -93,26 +94,81 @@ export class PlayCanvasDepthCursor {
|
|
|
93
94
|
const p = this.placer.update(views, { u: uv[0], v: uv[1], nearestPoint: this.hit, cursorHeight: this.height }, now);
|
|
94
95
|
this.placement = p;
|
|
95
96
|
this.pointer.hideCss(!!p.active);
|
|
97
|
+
const sprite = this._sprite || (p.active ? this._makeSprite(app) : null);
|
|
98
|
+
if (!sprite) return;
|
|
99
|
+
sprite.mi.visible = !!p.active;
|
|
96
100
|
if (!p.active) return;
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
app.drawLineArrays(pts, cols, false);
|
|
101
|
+
// The unit crosshair in the sprite's XY plane: basis (rotation), height (scale), position.
|
|
102
|
+
const { x, y, z } = p.basis;
|
|
103
|
+
const q = quatFromMatrix([x[0], x[1], x[2], 0, y[0], y[1], y[2], 0, z[0], z[1], z[2], 0, 0, 0, 0, 1]);
|
|
104
|
+
sprite.node.setLocalPosition(p.position[0], p.position[1], p.position[2]);
|
|
105
|
+
sprite.node.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
106
|
+
sprite.node.setLocalScale(p.height, p.height, p.height);
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
/** Mono / no pointer / no views: no sprite, the CSS cursor back. */
|
|
107
110
|
inactive() {
|
|
108
111
|
if (this.placement.active) this.placement = { active: false };
|
|
112
|
+
this.placer.update(null); // forget the hybrid anchor
|
|
113
|
+
if (this._sprite) this._sprite.mi.visible = false;
|
|
109
114
|
this.pointer.hideCss(false);
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
// The filled crosshair: one mesh (outline triangles first, fill after — the order a
|
|
118
|
+
// depth-test-off draw needs), vertex-coloured, alpha-blended, in the UI layer.
|
|
119
|
+
_makeSprite(app) {
|
|
120
|
+
const pc = this.pc;
|
|
121
|
+
const m = cursorCrosshairMesh();
|
|
122
|
+
const mesh = new pc.Mesh(app.graphicsDevice);
|
|
123
|
+
mesh.setPositions(m.positions);
|
|
124
|
+
mesh.setColors(m.colors);
|
|
125
|
+
mesh.update();
|
|
126
|
+
const mat = new pc.ShaderMaterial({
|
|
127
|
+
uniqueName: 'inline3dDepthCursor',
|
|
128
|
+
attributes: { vertex_position: pc.SEMANTIC_POSITION, vertex_color: pc.SEMANTIC_COLOR },
|
|
129
|
+
vertexGLSL: `
|
|
130
|
+
attribute vec3 vertex_position;
|
|
131
|
+
attribute vec4 vertex_color;
|
|
132
|
+
uniform mat4 matrix_model;
|
|
133
|
+
uniform mat4 matrix_viewProjection;
|
|
134
|
+
varying vec4 vColor;
|
|
135
|
+
void main() { vColor = vertex_color; gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0); }`,
|
|
136
|
+
fragmentGLSL: `
|
|
137
|
+
varying vec4 vColor;
|
|
138
|
+
void main() { gl_FragColor = vColor; }`,
|
|
139
|
+
});
|
|
140
|
+
mat.blendState = new pc.BlendState(
|
|
141
|
+
true,
|
|
142
|
+
pc.BLENDEQUATION_ADD,
|
|
143
|
+
pc.BLENDMODE_SRC_ALPHA,
|
|
144
|
+
pc.BLENDMODE_ONE_MINUS_SRC_ALPHA,
|
|
145
|
+
pc.BLENDEQUATION_ADD,
|
|
146
|
+
pc.BLENDMODE_ONE,
|
|
147
|
+
pc.BLENDMODE_ONE_MINUS_SRC_ALPHA,
|
|
148
|
+
);
|
|
149
|
+
mat.depthTest = false;
|
|
150
|
+
mat.depthWrite = false;
|
|
151
|
+
mat.cull = pc.CULLFACE_NONE;
|
|
152
|
+
mat.update();
|
|
153
|
+
const node = new pc.GraphNode('inline3d-cursor');
|
|
154
|
+
app.root.addChild(node); // world space = the views' (rig-carried) space
|
|
155
|
+
const mi = new pc.MeshInstance(mesh, mat, node);
|
|
156
|
+
mi.cull = false;
|
|
157
|
+
mi.pick = false;
|
|
158
|
+
mi.drawOrder = 1e9;
|
|
159
|
+
app.scene.layers.getLayerById(pc.LAYERID_UI).addMeshInstances([mi]);
|
|
160
|
+
this._sprite = { mi, node, mat };
|
|
161
|
+
return this._sprite;
|
|
162
|
+
}
|
|
163
|
+
|
|
112
164
|
dispose() {
|
|
113
165
|
this._disposed = true;
|
|
114
166
|
this.pointer.dispose();
|
|
115
167
|
try {
|
|
168
|
+
if (this._sprite) {
|
|
169
|
+
this.viewer.app?.scene?.layers?.getLayerById(this.pc.LAYERID_UI)?.removeMeshInstances?.([this._sprite.mi]);
|
|
170
|
+
this._sprite.node.destroy?.();
|
|
171
|
+
}
|
|
116
172
|
this._picker?.destroy?.();
|
|
117
173
|
this._pickCam?.destroy?.();
|
|
118
174
|
} catch {
|
|
@@ -27,8 +27,20 @@
|
|
|
27
27
|
// - the policy works in DISPARITY d = 1 - 1/t, in eye-baseline units (on-screen disparity =
|
|
28
28
|
// baseline × d): 0 on the canvas, < 0 in front. The eye compares disparities, so the margin,
|
|
29
29
|
// the clamp and the slew rates live there;
|
|
30
|
-
// - the sprite goes
|
|
31
|
-
//
|
|
30
|
+
// - WHERE along that depth the sprite goes is the anchor mode (CURSOR_ANCHOR_MODES):
|
|
31
|
+
// 'screen' — C = E + t (S - E), on the cyclopean ray: exactly over the pointer from where the
|
|
32
|
+
// viewer is, but head-motion invariant — disparity WITHOUT motion parallax, which
|
|
33
|
+
// on a head-tracked display reads as "at the glass";
|
|
34
|
+
// 'world' — C = S - f (1 - t) eyeToCanvas, straight in front of the pointer's canvas point:
|
|
35
|
+
// parallaxes like content, drifts ~mm off the pointer from an off-axis viewer;
|
|
36
|
+
// 'hybrid' (default) — 'screen' while the pointer moves (exact aim), and world-FIXED while it
|
|
37
|
+
// is still (correct parallax): the foot F of the last moving placement on the glass
|
|
38
|
+
// is kept, and C = F - f (1 - t) eyeToCanvas. Continuous at the stop frame.
|
|
39
|
+
// The height is scaled by t either way, so the apparent size is constant.
|
|
40
|
+
|
|
41
|
+
import { CURSOR_ANCHOR_MODES } from './inline3d-cursor-option.js';
|
|
42
|
+
|
|
43
|
+
export { CURSOR_ANCHOR_MODES, CURSOR_OPTION_KEYS, resolveCursorOption } from './inline3d-cursor-option.js';
|
|
32
44
|
|
|
33
45
|
/** Default sprite height, as a fraction of the canvas height. */
|
|
34
46
|
export const CURSOR_DEFAULT_HEIGHT = 0.03;
|
|
@@ -38,8 +50,12 @@ export const CURSOR_DEFAULT_HEIGHT = 0.03;
|
|
|
38
50
|
* Disparities are in eye-baseline units; times in seconds.
|
|
39
51
|
*/
|
|
40
52
|
export const CURSOR_DEFAULT_TUNING = Object.freeze({
|
|
41
|
-
/**
|
|
42
|
-
|
|
53
|
+
/**
|
|
54
|
+
* How far in front of the content the cursor floats. 0.005 baseline is ~1.5 mm at a 60 cm
|
|
55
|
+
* viewing distance: it RESTS on what it hovers. (1.36.0 shipped 0.03, ~1–2 cm, which a tester
|
|
56
|
+
* on a panel found floated visibly off shallow content.)
|
|
57
|
+
*/
|
|
58
|
+
margin: 0.005,
|
|
43
59
|
/** Comfort clamp: t >= 0.625, never more than ~3/8 of the way to the eye. */
|
|
44
60
|
minDisparity: -0.6,
|
|
45
61
|
maxDisparity: 0.6,
|
|
@@ -176,6 +192,10 @@ export function placeCursor(g, disparity, heightFraction) {
|
|
|
176
192
|
return { position: addScaled(g.eye, sub(g.canvasPoint, g.eye), t), height: hf * g.canvasHeight * t };
|
|
177
193
|
}
|
|
178
194
|
|
|
195
|
+
|
|
196
|
+
/** A pointer moving less than this (canvas-normalised) between frames counts as still. */
|
|
197
|
+
const POINTER_STILL_EPS = 1e-4;
|
|
198
|
+
|
|
179
199
|
/**
|
|
180
200
|
* The whole pipeline with its state: one per cursor.
|
|
181
201
|
*
|
|
@@ -189,23 +209,63 @@ export function placeCursor(g, disparity, heightFraction) {
|
|
|
189
209
|
* FOOTPRINT nearest the viewer, in the views' space, or null for "nothing under it".
|
|
190
210
|
*/
|
|
191
211
|
export class CursorDepthPlacer {
|
|
192
|
-
|
|
212
|
+
/**
|
|
213
|
+
* @param {object} [tuning=CURSOR_DEFAULT_TUNING] placement policy (see CursorTuning).
|
|
214
|
+
* @param {{anchor?: 'hybrid'|'screen'|'world'}} [opts]
|
|
215
|
+
*/
|
|
216
|
+
constructor(tuning = CURSOR_DEFAULT_TUNING, { anchor = 'hybrid' } = {}) {
|
|
217
|
+
if (!CURSOR_ANCHOR_MODES.includes(anchor)) {
|
|
218
|
+
throw new Error(`@displayxr/inline3d: cursor anchor "${anchor}" — expected ${CURSOR_ANCHOR_MODES.join(', ')}.`);
|
|
219
|
+
}
|
|
193
220
|
this.tuning = tuning;
|
|
221
|
+
this.anchor = anchor;
|
|
194
222
|
this.filter = {};
|
|
223
|
+
this._foot = null; // hybrid: the world-fixed foot on the glass while the pointer is still
|
|
224
|
+
this._last = null; // [u, v] of the previous active frame
|
|
195
225
|
}
|
|
196
226
|
|
|
197
227
|
update(views, hint, nowSec) {
|
|
198
228
|
const inactive = { active: false, position: null, basis: null, height: 0, disparity: 0, targetDisparity: 0 };
|
|
199
|
-
if (!views || views.length < 2 || !hint)
|
|
229
|
+
if (!views || views.length < 2 || !hint) {
|
|
230
|
+
this._foot = null;
|
|
231
|
+
this._last = null;
|
|
232
|
+
return inactive;
|
|
233
|
+
}
|
|
200
234
|
const a = asView(views[0]);
|
|
201
235
|
const b = asView(views[views.length - 1]);
|
|
202
236
|
const g = solveCursorGeometry(a, b, hint.u, hint.v);
|
|
203
|
-
if (!g)
|
|
237
|
+
if (!g) {
|
|
238
|
+
this._foot = null;
|
|
239
|
+
this._last = null;
|
|
240
|
+
return inactive;
|
|
241
|
+
}
|
|
204
242
|
const content = hint.nearestPoint ? cursorPointDisparity(g, hint.nearestPoint) : null;
|
|
205
243
|
const target = cursorTarget(this.tuning, content !== null, content);
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
244
|
+
const f = this.filter;
|
|
245
|
+
const reprimed = !f.primed || nowSec < f.last || nowSec - f.last > this.tuning.stale;
|
|
246
|
+
const disparity = cursorFilterStep(f, this.tuning, target, nowSec);
|
|
247
|
+
const { position: onRay, height } = placeCursor(g, disparity, hint.cursorHeight);
|
|
248
|
+
const t = 1 / (1 - disparity);
|
|
249
|
+
const out = (-(1 - t) * g.eyeToCanvas); // signed offset along the normal (toward the viewer)
|
|
250
|
+
const fw = g.forward;
|
|
251
|
+
let position = onRay;
|
|
252
|
+
let anchored = false;
|
|
253
|
+
if (this.anchor === 'world') {
|
|
254
|
+
position = addScaled(g.canvasPoint, fw, out);
|
|
255
|
+
} else if (this.anchor === 'hybrid') {
|
|
256
|
+
const last = this._last;
|
|
257
|
+
const moved =
|
|
258
|
+
reprimed || !this._foot || !last || Math.abs(hint.u - last[0]) > POINTER_STILL_EPS || Math.abs(hint.v - last[1]) > POINTER_STILL_EPS;
|
|
259
|
+
if (moved) {
|
|
260
|
+
// The foot of the on-ray placement on the glass, along the normal: C = F - f·(1-t)·eyeToCanvas.
|
|
261
|
+
this._foot = addScaled(onRay, fw, -out);
|
|
262
|
+
} else {
|
|
263
|
+
position = addScaled(this._foot, fw, out);
|
|
264
|
+
anchored = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this._last = [hint.u, hint.v];
|
|
268
|
+
return { active: true, position, basis: g.basis, height, disparity, targetDisparity: target, anchored, geometry: g };
|
|
209
269
|
}
|
|
210
270
|
}
|
|
211
271
|
|
|
@@ -219,22 +279,45 @@ function asView(v) {
|
|
|
219
279
|
* Constructing one adds two listeners; nothing else runs until a backend asks for `uv`.
|
|
220
280
|
*/
|
|
221
281
|
export class CursorPointer {
|
|
222
|
-
/**
|
|
223
|
-
|
|
282
|
+
/**
|
|
283
|
+
* @param {HTMLElement} canvas
|
|
284
|
+
* @param {{scope?: 'canvas'|'window'}} [opts] 'canvas' (default): the pointer counts only while
|
|
285
|
+
* it is over the canvas element itself. 'window': while it is inside the canvas's BOX,
|
|
286
|
+
* even over DOM layered on top of it (overlay buttons, labels) — and the CSS cursor is then
|
|
287
|
+
* hidden page-wide while the sprite shows, since the overlay has its own cursor.
|
|
288
|
+
*/
|
|
289
|
+
constructor(canvas, { scope = 'canvas' } = {}) {
|
|
290
|
+
if (scope !== 'canvas' && scope !== 'window') {
|
|
291
|
+
throw new Error(`@displayxr/inline3d: cursor pointerScope "${scope}" — expected 'canvas' or 'window'.`);
|
|
292
|
+
}
|
|
224
293
|
this.canvas = canvas;
|
|
294
|
+
this.scope = scope;
|
|
225
295
|
/** `[u, v]` while the pointer is over the canvas, else null. */
|
|
226
296
|
this.uv = null;
|
|
227
297
|
this._hidden = false;
|
|
228
298
|
this._prev = '';
|
|
229
299
|
this._onMove = (e) => {
|
|
230
300
|
const r = canvas.getBoundingClientRect();
|
|
231
|
-
|
|
301
|
+
if (!(r.width > 0 && r.height > 0)) return void (this.uv = null);
|
|
302
|
+
const u = (e.clientX - r.left) / r.width;
|
|
303
|
+
const v = (e.clientY - r.top) / r.height;
|
|
304
|
+
this.uv = u >= 0 && u <= 1 && v >= 0 && v <= 1 ? [u, v] : null;
|
|
232
305
|
};
|
|
233
306
|
this._onLeave = () => {
|
|
234
307
|
this.uv = null;
|
|
235
308
|
};
|
|
236
|
-
|
|
237
|
-
|
|
309
|
+
this._target = scope === 'window' ? globalThis : canvas;
|
|
310
|
+
this._target.addEventListener('pointermove', this._onMove);
|
|
311
|
+
if (scope === 'window') {
|
|
312
|
+
// Leaving the window (relatedTarget null) or losing focus mid-hover: nothing to hover.
|
|
313
|
+
this._onOut = (e) => {
|
|
314
|
+
if (!e.relatedTarget) this.uv = null;
|
|
315
|
+
};
|
|
316
|
+
globalThis.addEventListener('pointerout', this._onOut);
|
|
317
|
+
globalThis.addEventListener('blur', this._onLeave);
|
|
318
|
+
} else {
|
|
319
|
+
canvas.addEventListener('pointerleave', this._onLeave);
|
|
320
|
+
}
|
|
238
321
|
}
|
|
239
322
|
|
|
240
323
|
/** Set the pointer directly (canvas-normalised, v down), or null — for scripted input. */
|
|
@@ -251,7 +334,9 @@ export class CursorPointer {
|
|
|
251
334
|
/** Hide (true) or restore (false) the CSS cursor; idempotent. */
|
|
252
335
|
hideCss(hide) {
|
|
253
336
|
if (hide === this._hidden) return;
|
|
254
|
-
if (
|
|
337
|
+
if (this.scope === 'window') {
|
|
338
|
+
pageCursorHidden(hide);
|
|
339
|
+
} else if (hide) {
|
|
255
340
|
this._prev = this.canvas.style.cursor || '';
|
|
256
341
|
this.canvas.style.cursor = 'none';
|
|
257
342
|
} else {
|
|
@@ -261,12 +346,34 @@ export class CursorPointer {
|
|
|
261
346
|
}
|
|
262
347
|
|
|
263
348
|
dispose() {
|
|
264
|
-
this.
|
|
265
|
-
this.
|
|
349
|
+
this._target.removeEventListener('pointermove', this._onMove);
|
|
350
|
+
if (this.scope === 'window') {
|
|
351
|
+
globalThis.removeEventListener('pointerout', this._onOut);
|
|
352
|
+
globalThis.removeEventListener('blur', this._onLeave);
|
|
353
|
+
} else {
|
|
354
|
+
this.canvas.removeEventListener('pointerleave', this._onLeave);
|
|
355
|
+
}
|
|
266
356
|
this.hideCss(false);
|
|
267
357
|
}
|
|
268
358
|
}
|
|
269
359
|
|
|
360
|
+
// pointerScope 'window': the overlay DOM has its own cursor, so hiding it on the canvas alone is
|
|
361
|
+
// not enough. A class on <html> plus one injected rule hides it everywhere while any depth cursor
|
|
362
|
+
// shows; a count keeps two tiles from un-hiding each other.
|
|
363
|
+
let _pageHides = 0;
|
|
364
|
+
function pageCursorHidden(hide) {
|
|
365
|
+
const doc = globalThis.document;
|
|
366
|
+
if (!doc || !doc.documentElement) return;
|
|
367
|
+
if (hide && !doc.getElementById('inline3d-cursor-style')) {
|
|
368
|
+
const st = doc.createElement('style');
|
|
369
|
+
st.id = 'inline3d-cursor-style';
|
|
370
|
+
st.textContent = 'html.inline3d-cursor-hidden, html.inline3d-cursor-hidden * { cursor: none !important; }';
|
|
371
|
+
(doc.head || doc.documentElement).appendChild(st);
|
|
372
|
+
}
|
|
373
|
+
_pageHides = Math.max(0, _pageHides + (hide ? 1 : -1));
|
|
374
|
+
doc.documentElement.classList.toggle('inline3d-cursor-hidden', _pageHides > 0);
|
|
375
|
+
}
|
|
376
|
+
|
|
270
377
|
/**
|
|
271
378
|
* The footprint ring every backend samples: the hotspot plus 8 points on a circle of radius
|
|
272
379
|
* 0.75 × the sprite height (canvas heights), so the sprite and a margin are covered.
|
|
@@ -291,6 +398,53 @@ export const CURSOR_CROSSHAIR = Object.freeze([
|
|
|
291
398
|
[-0.15, -0.15, 0.15, -0.15], [0.15, -0.15, 0.15, 0.15], [0.15, 0.15, -0.15, 0.15], [-0.15, 0.15, -0.15, -0.15],
|
|
292
399
|
]);
|
|
293
400
|
|
|
401
|
+
/** Fill and outline stroke widths, in sprite heights; and their colours (RGBA, 0..1). */
|
|
402
|
+
const STROKE_FILL = 0.07;
|
|
403
|
+
const STROKE_OUTLINE = 0.15;
|
|
404
|
+
export const CURSOR_FILL_COLOR = Object.freeze([1, 0.84, 0.1, 1]);
|
|
405
|
+
export const CURSOR_OUTLINE_COLOR = Object.freeze([0.04, 0.04, 0.06, 0.85]);
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The crosshair as FILLED strokes in its own XY plane (unit = sprite height): a dark outline
|
|
409
|
+
* pass first, the coloured fill over it — the order a depth-test-off draw needs. A 1 px line
|
|
410
|
+
* reads too thin through a lenticular panel. Triangles, non-indexed.
|
|
411
|
+
* @returns {{positions: Float32Array, colors: Float32Array, count: number}}
|
|
412
|
+
*/
|
|
413
|
+
export function cursorCrosshairMesh(fill = CURSOR_FILL_COLOR, outline = CURSOR_OUTLINE_COLOR) {
|
|
414
|
+
const pos = [];
|
|
415
|
+
const col = [];
|
|
416
|
+
const quad = ([x0, y0, x1, y1], w, ext, c) => {
|
|
417
|
+
const dx = x1 - x0, dy = y1 - y0;
|
|
418
|
+
const l = Math.hypot(dx, dy) || 1;
|
|
419
|
+
const ux = dx / l, uy = dy / l; // along
|
|
420
|
+
const nx = -uy * (w / 2), ny = ux * (w / 2); // across
|
|
421
|
+
const ax = x0 - ux * ext, ay = y0 - uy * ext, bx = x1 + ux * ext, by = y1 + uy * ext;
|
|
422
|
+
const v = [[ax - nx, ay - ny], [bx - nx, by - ny], [bx + nx, by + ny], [ax - nx, ay - ny], [bx + nx, by + ny], [ax + nx, ay + ny]];
|
|
423
|
+
for (const [x, y] of v) {
|
|
424
|
+
pos.push(x, y, 0);
|
|
425
|
+
col.push(c[0], c[1], c[2], c[3]);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
for (const seg of CURSOR_CROSSHAIR) quad(seg, STROKE_OUTLINE, (STROKE_OUTLINE - STROKE_FILL) / 2, outline);
|
|
429
|
+
for (const seg of CURSOR_CROSSHAIR) quad(seg, STROKE_FILL, 0, fill);
|
|
430
|
+
return { positions: new Float32Array(pos), colors: new Float32Array(col), count: pos.length / 3 };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* A column-major model matrix placing the unit crosshair for a placement: the sprite's basis
|
|
435
|
+
* scaled by its height, at its position.
|
|
436
|
+
*/
|
|
437
|
+
export function cursorModelMatrix(placement, out = new Array(16)) {
|
|
438
|
+
const { x, y, z } = placement.basis;
|
|
439
|
+
const h = placement.height;
|
|
440
|
+
const e = placement.position;
|
|
441
|
+
out[0] = x[0] * h; out[1] = x[1] * h; out[2] = x[2] * h; out[3] = 0;
|
|
442
|
+
out[4] = y[0] * h; out[5] = y[1] * h; out[6] = y[2] * h; out[7] = 0;
|
|
443
|
+
out[8] = z[0] * h; out[9] = z[1] * h; out[10] = z[2] * h; out[11] = 0;
|
|
444
|
+
out[12] = e[0]; out[13] = e[1]; out[14] = e[2]; out[15] = 1;
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
|
|
294
448
|
/**
|
|
295
449
|
* The crosshair's segment endpoints in the views' space for a placement, as a flat
|
|
296
450
|
* [x, y, z, x, y, z, …] array (16 points) — what a line renderer takes.
|
|
@@ -309,3 +463,4 @@ export function cursorCrosshairPoints(placement, out = []) {
|
|
|
309
463
|
}
|
|
310
464
|
return out;
|
|
311
465
|
}
|
|
466
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// inline3d-cursor-option.js — `cursor: 'depth' | { … }` as ./model and ./splat accept it.
|
|
2
|
+
//
|
|
3
|
+
// Its own tiny, dependency-free module on purpose: the PlayCanvas adapter validates the option at
|
|
4
|
+
// the call (statically), but must not pull the cursor itself into a page that never asks for it
|
|
5
|
+
// (the cursor modules load only on opt-in). ./cursor-depth re-exports everything here.
|
|
6
|
+
|
|
7
|
+
/** Anchor modes — see ./inline3d-cursor-depth.js. */
|
|
8
|
+
export const CURSOR_ANCHOR_MODES = Object.freeze(['hybrid', 'screen', 'world']);
|
|
9
|
+
|
|
10
|
+
/** The keys `cursor: { … }` accepts on ./model and ./splat. */
|
|
11
|
+
export const CURSOR_OPTION_KEYS = Object.freeze(['height', 'margin', 'anchor', 'pointerScope']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `cursor` as ./model and ./splat accept it: `'depth'`, or an options object (which also means
|
|
15
|
+
* depth). Anything falsy is OFF — the zero-cost default. Validated here, at the call.
|
|
16
|
+
* @returns {null | {height?: number, margin?: number, anchor?: string, pointerScope?: string}}
|
|
17
|
+
*/
|
|
18
|
+
export function resolveCursorOption(opt, who = '@displayxr/inline3d') {
|
|
19
|
+
if (opt === undefined || opt === null || opt === false) return null;
|
|
20
|
+
if (opt === 'depth' || opt === true) return {};
|
|
21
|
+
if (typeof opt !== 'object') {
|
|
22
|
+
throw new Error(`${who}: cursor ${JSON.stringify(opt)} — expected 'depth' or { height, margin, anchor, pointerScope }.`);
|
|
23
|
+
}
|
|
24
|
+
const unknown = Object.keys(opt).filter((k) => !CURSOR_OPTION_KEYS.includes(k));
|
|
25
|
+
if (unknown.length) throw new Error(`${who}: cursor — unknown option(s) ${unknown.join(', ')}; expected ${CURSOR_OPTION_KEYS.join(', ')}.`);
|
|
26
|
+
if (opt.anchor !== undefined && !CURSOR_ANCHOR_MODES.includes(opt.anchor)) {
|
|
27
|
+
throw new Error(`${who}: cursor.anchor "${opt.anchor}" — expected ${CURSOR_ANCHOR_MODES.join(', ')}.`);
|
|
28
|
+
}
|
|
29
|
+
if (opt.pointerScope !== undefined && opt.pointerScope !== 'canvas' && opt.pointerScope !== 'window') {
|
|
30
|
+
throw new Error(`${who}: cursor.pointerScope "${opt.pointerScope}" — expected 'canvas' or 'window'.`);
|
|
31
|
+
}
|
|
32
|
+
for (const k of ['height', 'margin']) {
|
|
33
|
+
if (opt[k] !== undefined && !(typeof opt[k] === 'number' && opt[k] > 0 && Number.isFinite(opt[k]))) {
|
|
34
|
+
throw new Error(`${who}: cursor.${k} must be a positive number.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { ...opt };
|
|
38
|
+
}
|
package/js/inline3d-model.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
import * as THREE from 'three';
|
|
42
42
|
import { EyeCamera, EdgeFeather, DepthCursor, raycastHitTest } from './inline3d-three.js';
|
|
43
|
+
import { resolveCursorOption } from './inline3d-cursor-depth.js';
|
|
43
44
|
import { SceneViewer } from './inline3d-viewer.js';
|
|
44
45
|
// Procedural — built in memory, no asset to serve. Imported eagerly rather than lazily because it
|
|
45
46
|
// is the default lighting path, so deferring it would only add a frame of unlit content.
|
|
@@ -382,7 +383,8 @@ export function addModel(wall, canvas, src, opts = {}) {
|
|
|
382
383
|
logTag: '[inline3d/model]',
|
|
383
384
|
viewerEase: opts.viewerEase,
|
|
384
385
|
}).useEyeCamera(EyeCamera, EdgeFeather);
|
|
385
|
-
|
|
386
|
+
const cursorOpts = resolveCursorOption(opts.cursor, '@displayxr/inline3d/model');
|
|
387
|
+
if (cursorOpts) viewer.useDepthCursor(DepthCursor, (content) => raycastHitTest(THREE, [content]), cursorOpts);
|
|
386
388
|
|
|
387
389
|
if (envMap) viewer.scene.environment = envMap;
|
|
388
390
|
else if (environment === 'room') addRoomEnvironment(viewer);
|
|
@@ -120,6 +120,7 @@ import {
|
|
|
120
120
|
coverageExponent,
|
|
121
121
|
declareViewRig,
|
|
122
122
|
} from './inline3d-splat-shared.js';
|
|
123
|
+
import { resolveCursorOption } from './inline3d-cursor-option.js';
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
126
|
* The component systems the tile's `AppBase` registers. Camera + GSplat draw the splat; Render,
|
|
@@ -829,7 +830,7 @@ export class PlayCanvasSplatViewer {
|
|
|
829
830
|
// `cursor: 'depth'` (ADR-046): the depth-aware cursor. OPT-IN AND FREE OTHERWISE — its module
|
|
830
831
|
// is imported only when asked for (attachEngine), and every per-frame hook is an optional call
|
|
831
832
|
// on `cursorDepth`, which stays null without it.
|
|
832
|
-
this._cursorOpt = cursor;
|
|
833
|
+
this._cursorOpt = resolveCursorOption(cursor, opts.logTag || '[inline3d/splat]'); // null = off
|
|
833
834
|
this.cursorDepth = null;
|
|
834
835
|
// The tracking-acquisition ease (./inline3d-viewer-ease.js): undefined = the session's
|
|
835
836
|
// createInline3D({ viewerEase }) default. Built on the first 3D frame, which knows the session.
|
|
@@ -1414,10 +1415,11 @@ export class PlayCanvasSplatViewer {
|
|
|
1414
1415
|
if (this.featherPx > 0) this._makeFeather();
|
|
1415
1416
|
this._applyTransform();
|
|
1416
1417
|
app.start();
|
|
1417
|
-
if (this._cursorOpt
|
|
1418
|
+
if (this._cursorOpt) {
|
|
1419
|
+
const cursorOpts = this._cursorOpt;
|
|
1418
1420
|
import('./inline3d-cursor-depth-playcanvas.js')
|
|
1419
1421
|
.then(({ PlayCanvasDepthCursor }) => {
|
|
1420
|
-
if (!this._disposed) this.cursorDepth = new PlayCanvasDepthCursor(pc, this, { canvas: this.canvas });
|
|
1422
|
+
if (!this._disposed) this.cursorDepth = new PlayCanvasDepthCursor(pc, this, { ...cursorOpts, canvas: this.canvas });
|
|
1421
1423
|
})
|
|
1422
1424
|
.catch((err) => console.warn(`${this.logTag || '[inline3d/splat]'} cursor: 'depth' could not load; the normal cursor stays.`, err));
|
|
1423
1425
|
}
|
package/js/inline3d-splat.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import * as THREE from 'three';
|
|
22
22
|
import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';
|
|
23
23
|
import { EyeCamera, EdgeFeather, cameraRigFromCamera, DepthCursor, raycastHitTest } from './inline3d-three.js';
|
|
24
|
+
import { resolveCursorOption } from './inline3d-cursor-depth.js';
|
|
24
25
|
import { SceneViewer, boundsFromPositions } from './inline3d-viewer.js';
|
|
25
26
|
import { readSogCamera } from './inline3d-sog.js';
|
|
26
27
|
import { resolveRevealOption, effectsNotOnSpark } from './inline3d-splat-effects.js';
|
|
@@ -250,7 +251,8 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
250
251
|
}).useEyeCamera(EyeCamera, EdgeFeather);
|
|
251
252
|
// A gaussian-splat raycast is ~8 ms a ray on Spark: one ray a frame, cycling the footprint.
|
|
252
253
|
// (engine:'playcanvas' hit-tests with the engine's depth picker instead, which is far cheaper.)
|
|
253
|
-
|
|
254
|
+
const cursorOpts = resolveCursorOption(opts.cursor, '@displayxr/inline3d/splat');
|
|
255
|
+
if (cursorOpts) viewer.useDepthCursor(DepthCursor, (content) => raycastHitTest(THREE, [content]), { ...cursorOpts, raysPerFrame: 1 });
|
|
254
256
|
|
|
255
257
|
// Spark renders through the ordinary three.js pipeline, so splats and meshes co-exist and
|
|
256
258
|
// sort against each other — which is what lets a product page mix a captured hero with a
|
package/js/inline3d-three.js
CHANGED
|
@@ -52,7 +52,16 @@
|
|
|
52
52
|
// descriptor and nothing else: no Kooima, no off-axis math, no scale, here or anywhere in this
|
|
53
53
|
// SDK. That stays in the runtime, which is the point of the extension.
|
|
54
54
|
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
CursorDepthPlacer,
|
|
57
|
+
CursorPointer,
|
|
58
|
+
cursorCrosshairMesh,
|
|
59
|
+
cursorFootprint,
|
|
60
|
+
cursorModelMatrix,
|
|
61
|
+
cursorViewRay,
|
|
62
|
+
CURSOR_DEFAULT_HEIGHT,
|
|
63
|
+
CURSOR_DEFAULT_TUNING,
|
|
64
|
+
} from './inline3d-cursor-depth.js';
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* A reusable three.js camera driven directly by an XRView's matrices. Construct once with
|
|
@@ -473,14 +482,24 @@ export class DepthCursor {
|
|
|
473
482
|
* content point along a ray in WORLD space, or null. {@link raycastHitTest} for meshes;
|
|
474
483
|
* for splats return the renderer's expected depth along the ray (never raycast splats).
|
|
475
484
|
* @param {number} [opts.height=0.03] sprite height as a fraction of the canvas height.
|
|
476
|
-
* @param {number} [opts.color=
|
|
485
|
+
* @param {number} [opts.color=0xffd61a] fill colour (the outline is always dark).
|
|
486
|
+
* @param {number} [opts.margin=0.005] how far in front of the content it floats, in eye-baseline
|
|
487
|
+
* units (0.005 ≈ 1.5 mm at 60 cm: it rests on the content).
|
|
488
|
+
* @param {'hybrid'|'screen'|'world'} [opts.anchor='hybrid'] where along that depth: 'hybrid'
|
|
489
|
+
* follows the pointer exactly while it moves and stays world-fixed (parallaxes with the
|
|
490
|
+
* content) while it is still; 'screen' never parallaxes; 'world' always does.
|
|
491
|
+
* @param {'canvas'|'window'} [opts.pointerScope='canvas'] 'window' keeps the cursor over DOM
|
|
492
|
+
* layered on the canvas (overlay buttons) and hides the CSS cursor page-wide meanwhile.
|
|
477
493
|
* @param {object} [opts.viewSpace] Object3D whose world matrix maps view transforms to world.
|
|
478
494
|
* @param {number} [opts.raysPerFrame=0] 0 = the whole footprint every frame (two eyes × 9
|
|
479
495
|
* points). N > 0 = an EXPENSIVE hit test (e.g. a gaussian-splat raycast, ~8 ms a ray):
|
|
480
496
|
* cast N rays a frame from the first eye, cycling through the footprint, and take the
|
|
481
497
|
* nearest of the most recent full cycle.
|
|
482
498
|
*/
|
|
483
|
-
constructor(
|
|
499
|
+
constructor(
|
|
500
|
+
THREE,
|
|
501
|
+
{ canvas, hitTest, height = CURSOR_DEFAULT_HEIGHT, color = 0xffd61a, viewSpace = null, raysPerFrame = 0, margin, anchor = 'hybrid', pointerScope = 'canvas' },
|
|
502
|
+
) {
|
|
484
503
|
this._THREE = THREE;
|
|
485
504
|
this.canvas = canvas;
|
|
486
505
|
this.hitTest = hitTest;
|
|
@@ -489,17 +508,28 @@ export class DepthCursor {
|
|
|
489
508
|
this.raysPerFrame = raysPerFrame;
|
|
490
509
|
this._ring = []; // amortised mode: the last hit per footprint point (null = a miss)
|
|
491
510
|
this._next = 0;
|
|
492
|
-
|
|
493
|
-
this.
|
|
494
|
-
|
|
511
|
+
const tuning = margin > 0 && Number.isFinite(margin) ? { ...CURSOR_DEFAULT_TUNING, margin } : CURSOR_DEFAULT_TUNING;
|
|
512
|
+
this.placer = new CursorDepthPlacer(tuning, { anchor });
|
|
513
|
+
this.pointer = new CursorPointer(canvas, { scope: pointerScope });
|
|
514
|
+
/** The last placement (diagnostics): `{active, position, height, disparity, targetDisparity, anchored}`. */
|
|
495
515
|
this.placement = { active: false };
|
|
496
516
|
|
|
497
|
-
|
|
498
|
-
|
|
517
|
+
// Filled strokes with a dark outline (a 1 px line reads too thin through the lens).
|
|
518
|
+
const fill = [((color >> 16) & 255) / 255, ((color >> 8) & 255) / 255, (color & 255) / 255, 1];
|
|
519
|
+
const m = cursorCrosshairMesh(fill);
|
|
499
520
|
const geom = new THREE.BufferGeometry();
|
|
500
|
-
geom.setAttribute('position', new THREE.Float32BufferAttribute(
|
|
501
|
-
|
|
502
|
-
|
|
521
|
+
geom.setAttribute('position', new THREE.Float32BufferAttribute(m.positions, 3));
|
|
522
|
+
geom.setAttribute('color', new THREE.Float32BufferAttribute(m.colors, 4));
|
|
523
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
524
|
+
vertexColors: true,
|
|
525
|
+
transparent: true,
|
|
526
|
+
depthTest: false,
|
|
527
|
+
depthWrite: false,
|
|
528
|
+
side: THREE.DoubleSide,
|
|
529
|
+
toneMapped: false,
|
|
530
|
+
});
|
|
531
|
+
this.object = new THREE.Mesh(geom, mat);
|
|
532
|
+
this.object.name = 'inline3d-cursor';
|
|
503
533
|
this.object.matrixAutoUpdate = false;
|
|
504
534
|
this.object.frustumCulled = false;
|
|
505
535
|
this.object.renderOrder = 1e9; // last: never occluded, it is in front by construction
|
|
@@ -528,15 +558,7 @@ export class DepthCursor {
|
|
|
528
558
|
this.placement = p;
|
|
529
559
|
this.object.visible = !!p.active;
|
|
530
560
|
if (p.active) {
|
|
531
|
-
|
|
532
|
-
const h = p.height;
|
|
533
|
-
const e = p.position;
|
|
534
|
-
this.object.matrix.fromArray([
|
|
535
|
-
x[0] * h, x[1] * h, x[2] * h, 0,
|
|
536
|
-
y[0] * h, y[1] * h, y[2] * h, 0,
|
|
537
|
-
z[0] * h, z[1] * h, z[2] * h, 0,
|
|
538
|
-
e[0], e[1], e[2], 1,
|
|
539
|
-
]);
|
|
561
|
+
this.object.matrix.fromArray(cursorModelMatrix(p));
|
|
540
562
|
this.object.matrixWorldNeedsUpdate = true;
|
|
541
563
|
}
|
|
542
564
|
this.pointer.hideCss(!!p.active);
|
package/model.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Type definitions for @displayxr/inline3d/model (and ./model/three).
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
|
+
import type { CursorOptions } from './cursor-depth.js';
|
|
4
5
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
5
6
|
import type { FirstWovenResult, ViewerEaseOption } from './index.js';
|
|
6
7
|
import type { PlayCanvasSplatViewer, SplatCameraPose, SplatFrameInfo } from './splat.js';
|
|
@@ -55,9 +56,10 @@ export interface ModelOptions {
|
|
|
55
56
|
* is hidden while it shows and comes back in 2D and off the canvas. OPT-IN: without it nothing
|
|
56
57
|
* is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
|
|
57
58
|
* one low-res pass every other frame while the pointer is over the tile). three: a raycast
|
|
58
|
-
* against the subject.
|
|
59
|
+
* against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope }`
|
|
60
|
+
* (any object also means depth).
|
|
59
61
|
*/
|
|
60
|
-
cursor?: 'depth';
|
|
62
|
+
cursor?: 'depth' | CursorOptions;
|
|
61
63
|
/**
|
|
62
64
|
* How the mesh is lit.
|
|
63
65
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.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",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"js/inline3d-buffer-limit.js",
|
|
75
75
|
"js/inline3d-viewer-ease.js",
|
|
76
76
|
"js/inline3d-three.js",
|
|
77
|
+
"js/inline3d-cursor-option.js",
|
|
77
78
|
"js/inline3d-cursor-depth.js",
|
|
78
79
|
"js/inline3d-cursor-depth-playcanvas.js",
|
|
79
80
|
"js/inline3d-viewer.js",
|
package/splat.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Type definitions for @displayxr/inline3d/splat.
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
|
+
import type { CursorOptions } from './cursor-depth.js';
|
|
4
5
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
5
6
|
import type { FirstWovenResult, ViewerEaseOption, DisplayMetrics } from './index.js';
|
|
6
7
|
|
|
@@ -479,9 +480,10 @@ export interface SplatOptions {
|
|
|
479
480
|
* is hidden while it shows and comes back in 2D and off the canvas. OPT-IN: without it nothing
|
|
480
481
|
* is loaded or run. PlayCanvas: hit-tested with the engine's depth picker (meshes and splats,
|
|
481
482
|
* one low-res pass every other frame while the pointer is over the tile). three: a raycast
|
|
482
|
-
* against the subject.
|
|
483
|
+
* against the subject. Pass an object to tune it: `{ margin, anchor, height, pointerScope }`
|
|
484
|
+
* (any object also means depth).
|
|
483
485
|
*/
|
|
484
|
-
cursor?: 'depth';
|
|
486
|
+
cursor?: 'depth' | CursorOptions;
|
|
485
487
|
/** Camera rig only: the distance in world metres that sits ON the glass. */
|
|
486
488
|
convergence?: number;
|
|
487
489
|
/**
|
package/three.d.ts
CHANGED
|
@@ -143,7 +143,7 @@ export class EdgeFeather {
|
|
|
143
143
|
): void;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
import type { CursorPlacement, CursorView } from './cursor-depth.js';
|
|
146
|
+
import type { CursorAnchorMode, CursorPlacement, CursorView } from './cursor-depth.js';
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
149
|
* A hit test for {@link DepthCursor} that raycasts three.js objects (recursively): the first hit
|
|
@@ -174,13 +174,21 @@ export class DepthCursor {
|
|
|
174
174
|
hitTest: (origin: number[], direction: number[]) => number[] | null;
|
|
175
175
|
/** Sprite height as a fraction of the canvas height (default 0.03). */
|
|
176
176
|
height?: number;
|
|
177
|
-
/**
|
|
177
|
+
/** Fill colour of the crosshair (default 0xffd61a; the outline is always dark). */
|
|
178
178
|
color?: number;
|
|
179
|
+
/** How far in front of the content it floats, in eye-baseline units (default 0.005 ≈ 1.5 mm at 60 cm). */
|
|
180
|
+
margin?: number;
|
|
181
|
+
/** Default `'hybrid'`: exact while the pointer moves, world-fixed (parallaxing) while it is still. */
|
|
182
|
+
anchor?: CursorAnchorMode;
|
|
183
|
+
/** `'window'`: keep the cursor over DOM layered on the canvas; hides the CSS cursor page-wide meanwhile. */
|
|
184
|
+
pointerScope?: 'canvas' | 'window';
|
|
185
|
+
/** An EXPENSIVE hit test: N rays a frame from the first eye, cycling the footprint (0 = all). */
|
|
186
|
+
raysPerFrame?: number;
|
|
179
187
|
/** Attach pattern: the Object3D the eye cameras hang off; its world matrix carries the views to world. */
|
|
180
188
|
viewSpace?: unknown;
|
|
181
189
|
},
|
|
182
190
|
);
|
|
183
|
-
/** The crosshair (a `THREE.
|
|
191
|
+
/** The crosshair (a `THREE.Mesh` of filled, outlined strokes; depth test off, drawn last). Add it to the scene root. */
|
|
184
192
|
readonly object: unknown;
|
|
185
193
|
/** The last placement, for diagnostics. */
|
|
186
194
|
readonly placement: CursorPlacement | { active: false };
|