@displayxr/inline3d 1.2.1 → 1.4.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 +155 -0
- package/README.md +20 -0
- package/index.d.ts +335 -0
- package/js/inline3d-model.js +1 -1
- package/js/inline3d-three.js +188 -0
- package/js/inline3d-undock.js +251 -0
- package/js/inline3d.js +885 -7
- package/package.json +6 -1
- package/three.d.ts +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,161 @@ 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.4.0 — 2026-09-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Display modes — the page can read what the panel IS, and ask it to change.** Until now a page
|
|
13
|
+
could describe its own framing (a view rig) but knew nothing about the display it was framing
|
|
14
|
+
*for*: not its size in metres, not its pixel count, not which of the runtime's rendering modes it
|
|
15
|
+
was in, and it had no way to ask for a different one. Three pass-throughs close that:
|
|
16
|
+
**`getDisplayInfo()`** (physical size, pixel size, recommended view scale; `null` where there is
|
|
17
|
+
no glasses-free display), **`getRenderingModes()`** (every mode the runtime can put the display
|
|
18
|
+
in — view count, tile grid, per-view pixels, `hardwareDisplay3D`, `isActive`, `isRequestable`)
|
|
19
|
+
and **`requestRenderingMode(i)`**. They sit on the **`createInline3D()` result** — the panel is
|
|
20
|
+
the document's, not a tile's — and under the same names on every tile handle, routed to whichever
|
|
21
|
+
window currently holds a live layer. Gate the group with
|
|
22
|
+
**`inline3dDisplayModesSupported()`**, which requires all three methods on
|
|
23
|
+
`XRDisplayLayer.prototype` — a browser shipping half the set is one mid-implementation, and
|
|
24
|
+
calling it supported would surface as a `not a function` inside a click handler.
|
|
25
|
+
*(core tier — additive)*
|
|
26
|
+
|
|
27
|
+
- **The 2D/3D hardware state is a consequence of the mode, not a control.** There is deliberately
|
|
28
|
+
**no page-facing request** for it. Asking for a mode with `viewCount === 1` puts the panel in its
|
|
29
|
+
2D state and the browser reports that mode active — the runtime carries on weaving the same fixed
|
|
30
|
+
two-view atlas — and asking for the 2-view mode puts it back. Tying the two together makes the
|
|
31
|
+
one bad state (a flat panel showing a stereo atlas, i.e. a blurry double image rather than 2D)
|
|
32
|
+
unreachable. **`requestDisplayMode('2d'|'3d')` is gone**, and so is every mention of a "lens".
|
|
33
|
+
|
|
34
|
+
- **The SDK collapses the stereo rig automatically.** When a 1-view mode goes **active** every
|
|
35
|
+
window's `ipdFactor`/`parallaxFactor` is pushed to 0 (both eyes render from one place), and a
|
|
36
|
+
2-view mode going active restores them. This is driven by the `renderingmodechange` event — plus
|
|
37
|
+
the first `getRenderingModes()` read, so a page that OPENS with the panel already flat is
|
|
38
|
+
collapsed too — and **not** by the request. So it happens however the mode changed (this page,
|
|
39
|
+
another one, the shell), the page's own render loop is untouched, and **a refused request changes
|
|
40
|
+
nothing in either direction**, which is now structural rather than a rollback.
|
|
41
|
+
|
|
42
|
+
**The restore is exact, and that is a design property, not luck.** The flattening is a *copy*
|
|
43
|
+
pushed at the layer — a page driving a rig every frame reuses one descriptor object, so zeroing
|
|
44
|
+
it in place would write the flattening into the page's own state and the restore would restore
|
|
45
|
+
0. So each window keeps what the page asked for, untouched, and the flat rig is derived on the
|
|
46
|
+
way out. That latch also means a per-frame `setViewRig` loop cannot walk the page out of 2D, a
|
|
47
|
+
lazy tile that scrolls away and rebuilds its layer comes back flat rather than in 3D, and a
|
|
48
|
+
window that never set a rig at all is flattened (and restored) via the exact rig equivalent of
|
|
49
|
+
its `virtualDisplayHeight`. *(core tier — additive)*
|
|
50
|
+
|
|
51
|
+
- **`setStereoEnabled(bool)` is now SUGAR, and only sugar** — `false` requests the first mode with
|
|
52
|
+
`viewCount === 1 && isRequestable`, `true` the first with `viewCount === 2 && isRequestable`. It
|
|
53
|
+
touches neither the hardware state (nothing can) nor the rig (the event does that). It rejects
|
|
54
|
+
rather than inventing a mode when the panel lists none. *(core tier — behaviour change on an
|
|
55
|
+
unreleased API)*
|
|
56
|
+
|
|
57
|
+
- **`on(type, cb)` / `off(type, cb)`** — the two events, which fire on the **XRSession** and not on
|
|
58
|
+
the layer, re-emitted on the wall and on every handle: `renderingmodechange`
|
|
59
|
+
`{type, modeIndex, viewCount, mode, detail}` and `hardwaredisplaystatechange`
|
|
60
|
+
`{type, state:'2d'|'3d', detail}`. `on` returns an unsubscribe function;
|
|
61
|
+
**`onDisplayModeChange(cb)`** remains as the both-events-one-callback shape.
|
|
62
|
+
`wall.hardwareDisplayState`, `wall.activeMode` and `wall.stereoCollapsed` expose the last
|
|
63
|
+
**reported** state — never the last requested one. *(core tier — additive)*
|
|
64
|
+
|
|
65
|
+
- **Undock — lift a window's asset into a floating native viewer over the desktop.**
|
|
66
|
+
**`wall.undock`** is `{model, splat}` on a browser with `XRDisplayLayer.undock` and **`null`** on
|
|
67
|
+
one without (plain Chrome, or an older DisplayXR browser) — that null is what a page branches on;
|
|
68
|
+
**`wall.refreshUndock()`** re-reads it, **`inline3dUndockSupported()`** is the sync probe. The
|
|
69
|
+
action is **`await undock(element, {src, type, env?, pose?, margin?, title?})`** (new module
|
|
70
|
+
`js/inline3d-undock.js`, re-exported from the main entry): API-first through `layer.undock()`,
|
|
71
|
+
falling back to the `displayxr-view:` OS protocol (hidden-iframe navigation, Chrome's one-time
|
|
72
|
+
"Open DisplayXR…?" prompt) where the layer method is absent. **Call it synchronously inside the
|
|
73
|
+
click** — both paths need the transient activation. Resolves to `{ended, viewer, detached}`;
|
|
74
|
+
rejects with an Error named `not-installed` | `src-not-allowed` | `no-activation` | `busy`.
|
|
75
|
+
*(core tier — additive)*
|
|
76
|
+
|
|
77
|
+
- **[`samples/display-modes/`](samples/display-modes/)** — the whole surface on one page: the
|
|
78
|
+
`getDisplayInfo()` fields, the `getRenderingModes()` table with the active row marked, a request
|
|
79
|
+
button on every requestable row (`viewCount` 1 **or** 2) and the rows needing more than two views
|
|
80
|
+
greyed with the reason, one `setStereoEnabled` convenience button, a **read-only** hardware
|
|
81
|
+
display state badge fed by the event, and a live event log. Every action prints a greppable
|
|
82
|
+
`[display-modes] …` line so a harness can drive it from the console.
|
|
83
|
+
|
|
84
|
+
### Notes
|
|
85
|
+
|
|
86
|
+
- **Advisory scales.** `viewScaleX/Y` and `recommendedViewScaleX/Y` are what the runtime would like
|
|
87
|
+
the per-view resolution to be. The browser cannot resize a page's canvas, so nothing applies them
|
|
88
|
+
for you — a page honours them by sizing its own backing store. Ignoring them costs sharpness or
|
|
89
|
+
fill rate, never correctness.
|
|
90
|
+
- **The browser is fixed at two views.** No view synthesis exists anywhere in this stack, so a mode
|
|
91
|
+
needing MORE than two is listed (the panel really can do it) and refused. Show those rows; mark
|
|
92
|
+
them. A **one**-view mode is requestable — the browser still submits two views and the runtime
|
|
93
|
+
still weaves them; it is the panel that goes flat.
|
|
94
|
+
|
|
95
|
+
## 1.3.0 — 2026-09-04
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- **View rigs — the page can hand the runtime its own CAMERA, not just a virtual-display height.**
|
|
100
|
+
Every inline-3D frame is located against a *view rig*, and until now there was exactly one: a
|
|
101
|
+
display rig with an identity pose whose only knob was `virtualDisplayHeight`. That is the right
|
|
102
|
+
model for a portal — the canvas is a window onto a scene authored to fit it — and the wrong one
|
|
103
|
+
for a scene that owns a camera. An orbit, a walkthrough, a game has a pose and a field of view
|
|
104
|
+
already; it does not want to be told where the eyes are, it wants its own frustum perturbed by
|
|
105
|
+
them. That could not be expressed at all, so those pages either fought the display rig or
|
|
106
|
+
re-derived stereo themselves.
|
|
107
|
+
|
|
108
|
+
`handle.setViewRig(rig)` and `addScene`'s `viewRig` option send the whole descriptor:
|
|
109
|
+
`{type:'display'|'camera', position, orientation, virtualDisplayHeight, ipdFactor,
|
|
110
|
+
parallaxFactor, perspectiveFactor, convergenceDiopters, verticalFov, metersToVirtual}`. A rig
|
|
111
|
+
applies per-locate, so animating one is just sending new values each frame — nothing to tween,
|
|
112
|
+
nothing to tear down. **No projection math lands in the SDK**: it fills in a descriptor and the
|
|
113
|
+
off-axis (Kooima) frustum stays in the runtime, which is the same code the native apps consume.
|
|
114
|
+
*(core tier — additive)*
|
|
115
|
+
|
|
116
|
+
- **`inline3dViewRigSupported()`**, and a fallback that actually falls back. The gate reads a
|
|
117
|
+
capability (`XRDisplayLayer.prototype.setViewRig` being present), never a version or UA string.
|
|
118
|
+
Without it `setViewRig()` warns once and returns `false` while the window keeps weaving — so a
|
|
119
|
+
page that merely wants the extra control where it exists can call it unconditionally. And a
|
|
120
|
+
`viewRig` is only put in the layer init on a browser that *has* rigs: an older one would take
|
|
121
|
+
the init, find no member it recognised, and drop to its **own** default height, so passing a
|
|
122
|
+
camera rig alongside a `virtualDisplayHeight` now genuinely names the older browser's framing.
|
|
123
|
+
*(core tier — additive)*
|
|
124
|
+
|
|
125
|
+
- **`cameraRigFromCamera(THREE, camera, opts)` and `displayRig(opts)`** in
|
|
126
|
+
`@displayxr/inline3d/three` — descriptor builders, both accepting an `out` object so a per-frame
|
|
127
|
+
call allocates nothing. `cameraRigFromCamera` decomposes the pose from `matrixWorld` rather than
|
|
128
|
+
reading `.position`/`.quaternion` (those are local, and an app camera parented under a dolly —
|
|
129
|
+
the usual way to build an orbit — would otherwise report a pose in the wrong space), converts
|
|
130
|
+
three's degrees to the descriptor's radians, and turns a convergence *distance* into diopters so
|
|
131
|
+
"infinity" is a finite `0`. *(core tier — additive)*
|
|
132
|
+
|
|
133
|
+
- **`EyeCamera.setLocalFromView(view)` / `setLocalFromMatrices(proj, transform)`, for the attach
|
|
134
|
+
pattern.** The browser locates views **before** the page's rAF, so a rig set during frame N
|
|
135
|
+
drives the views delivered in frame N+1. On a slider that is invisible; on a camera moving under
|
|
136
|
+
the pointer it reads as a soft, swimming misalignment. The fix is not prediction: send an
|
|
137
|
+
identity-posed camera rig and parent the eye cameras under the app camera, and three's scene
|
|
138
|
+
graph composes *this* frame's world pose with no lag. These setters write `camera.matrix` and
|
|
139
|
+
leave `matrixWorld` to three's traversal, which is the whole of it — a scene-graph parent, not
|
|
140
|
+
projection math. `setFromView` (world) is unchanged. *(core tier — additive)*
|
|
141
|
+
|
|
142
|
+
- **`samples/camera-rig/`** — an orbiting scene on a camera rig, with sliders for FOV, convergence
|
|
143
|
+
and distance, an `attach` toggle, a live comfort readout, and `C` to cut between the camera rig
|
|
144
|
+
and a display rig framed to match it at the home angle. `samples/hello-cube/?debug` gains the
|
|
145
|
+
display rig's knobs; `samples/windows/`'s live scene tile moves to a camera rig in the attach
|
|
146
|
+
pattern. The comfort rule the readout prints is the runtime's own
|
|
147
|
+
(`ipdFactor × metersToVirtual × convergenceDiopters × N`, N ≈ 0.5 m): at 1 the viewer's eyes are
|
|
148
|
+
parallel on infinitely far content and past it they diverge. The SDK documents it and never
|
|
149
|
+
enforces it — the runtime clamps its own inputs, once, with a warning.
|
|
150
|
+
|
|
151
|
+
### Docs
|
|
152
|
+
|
|
153
|
+
- **[`docs/porting-three-js-apps.md`](docs/porting-three-js-apps.md) — porting an existing three.js
|
|
154
|
+
app (WebXR or plain) to inline 3D.** The rig work above closed the gap that made this guide
|
|
155
|
+
possible: an app that owns a camera can now hand it over, so "port your WebXR app" stops meaning
|
|
156
|
+
"re-author it as a portal". The guide is the WebXR→inline-3d mapping table (what each of
|
|
157
|
+
`isSessionSupported`, `renderer.xr`, `setAnimationLoop`, `XRWebGLLayer`, reference spaces,
|
|
158
|
+
offset-reference-space locomotion, `ArrayCamera`, controllers and `updateRenderState` becomes, and
|
|
159
|
+
why), the whole render loop with validate-before-clear and last-good replay, the Spark
|
|
160
|
+
double-sort, DOM UI over a woven canvas, picking, a hardware checklist, and a 24-item pitfalls
|
|
161
|
+
register. Linked from the README and from the top of the authoring guide.
|
|
162
|
+
|
|
8
163
|
## 1.2.1 — 2026-08-25
|
|
9
164
|
|
|
10
165
|
### Fixed
|
package/README.md
CHANGED
|
@@ -65,6 +65,19 @@ if (!wall.supported) {
|
|
|
65
65
|
The browser weaves each element's stereo pair at its on-screen rect; the surrounding DOM stays flat.
|
|
66
66
|
The runtime batches every visible window into one weave per frame, so it scales to a wall of elements.
|
|
67
67
|
|
|
68
|
+
A scene with its own camera can hand that camera to the runtime instead of being told where the eyes
|
|
69
|
+
are — a **camera rig** — and let eye tracking perturb its frustum:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { cameraRigFromCamera } from '@displayxr/inline3d/three';
|
|
73
|
+
const handle = wall.addScene(canvas, onFrame, { viewRig: cameraRigFromCamera(THREE, cam, { convergence: 1.2 }) });
|
|
74
|
+
handle.setViewRig(cameraRigFromCamera(THREE, cam, { convergence: 1.2, out: rig })); // per frame
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
No projection math lands in your page or in the SDK — the off-axis frustum stays in the runtime. See
|
|
78
|
+
[view rigs](docs/authoring-inline-3d.md#view-rigs-display-vs-camera) and
|
|
79
|
+
[`samples/camera-rig/`](samples/camera-rig/).
|
|
80
|
+
|
|
68
81
|
> **Detection:** call `createInline3D()` and check `wall.supported` — do **not** gate on
|
|
69
82
|
> `navigator.xr.isSessionSupported('inline-3d')`. That async probe resolves `false` if it runs before the
|
|
70
83
|
> OS weave service has bound (typically at page load), a false-negative that silently drops you to 2D.
|
|
@@ -78,6 +91,8 @@ Three.js glue (an off-axis `EyeCamera`) in [`js/inline3d-three.js`](js/inline3d-
|
|
|
78
91
|
```
|
|
79
92
|
index.html landing (Pages entry point)
|
|
80
93
|
samples/
|
|
94
|
+
camera-rig/ the camera rig — an orbiting scene that sends its OWN camera each frame;
|
|
95
|
+
convergence + comfort, the attach pattern, C to A/B a display rig
|
|
81
96
|
windows/ mixed 3D windows — still photos + a live video + a real-time three.js scene,
|
|
82
97
|
each woven with one SDK call, all on one session
|
|
83
98
|
splat/ a 3D Gaussian splat in a tile, auto-framed, with a 2D price plate over it
|
|
@@ -95,6 +110,11 @@ js/
|
|
|
95
110
|
from what the asset declares (you serve the decoder files — see the guide)
|
|
96
111
|
docs/
|
|
97
112
|
authoring-inline-3d.md the authoring guide
|
|
113
|
+
authoring-motion-and-effects.md
|
|
114
|
+
motion, transitions and per-eye effects — the half of authoring
|
|
115
|
+
that is not the API
|
|
116
|
+
porting-three-js-apps.md porting an existing three.js app (WebXR or plain) to inline 3D —
|
|
117
|
+
the WebXR→inline-3d mapping table and the whole render loop
|
|
98
118
|
```
|
|
99
119
|
|
|
100
120
|
## The inline-3D model (under the SDK)
|
package/index.d.ts
CHANGED
|
@@ -13,6 +13,153 @@ export interface TileOptions {
|
|
|
13
13
|
feather?: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A view-rig descriptor — what the runtime locates the eye views against. Two rigs, one shape:
|
|
18
|
+
*
|
|
19
|
+
* - **`"display"`** — the canvas is a PORTAL. Its plane is world `z = 0` and the viewer looks
|
|
20
|
+
* through it at a virtual display `virtualDisplayHeight` tall. This is the default rig and
|
|
21
|
+
* what `SceneOptions.virtualDisplayHeight` is shorthand for (identity pose, all factors 1).
|
|
22
|
+
* - **`"camera"`** — an APP CAMERA whose frustum eye tracking perturbs. The runtime keeps your
|
|
23
|
+
* `verticalFov`, offsets the eyes, and skews each frustum so `convergenceDiopters` lands on
|
|
24
|
+
* the zero-disparity plane.
|
|
25
|
+
*
|
|
26
|
+
* Every field is optional; the runtime **clamps** an out-of-range value (once, with a warning)
|
|
27
|
+
* rather than rejecting the rig. A descriptor applies per-locate, so animating one means sending
|
|
28
|
+
* new values each frame — see {@link TileHandle.setViewRig} for the one-frame latency that
|
|
29
|
+
* implies. Build one with `cameraRigFromCamera` / `displayRig` from
|
|
30
|
+
* `@displayxr/inline3d/three`, or by hand.
|
|
31
|
+
*/
|
|
32
|
+
export interface XRViewRigInit {
|
|
33
|
+
/** `"display"` (portal) or `"camera"` (app camera). */
|
|
34
|
+
type?: 'display' | 'camera';
|
|
35
|
+
/** Rig pose in the app's world units (default 0,0,0). */
|
|
36
|
+
position?: DOMPointInit;
|
|
37
|
+
/** Rig orientation as a quaternion (default identity). */
|
|
38
|
+
orientation?: DOMPointInit;
|
|
39
|
+
/** **Display rig.** Metres of virtual display; `m2v = this / the element's physical height`. */
|
|
40
|
+
virtualDisplayHeight?: number;
|
|
41
|
+
/** Eye separation — display rig: relative `[0,1]`; camera rig: absolute `>= 0`. */
|
|
42
|
+
ipdFactor?: number;
|
|
43
|
+
/** Head-tracking response — display rig: `[0,1]`; camera rig: absolute `>= 0`. */
|
|
44
|
+
parallaxFactor?: number;
|
|
45
|
+
/** **Display rig only**, `[0.1,10]`: exaggerates or flattens the off-axis skew. */
|
|
46
|
+
perspectiveFactor?: number;
|
|
47
|
+
/** **Camera rig.** `1 / (convergence distance in world units)`; `0` = infinity. */
|
|
48
|
+
convergenceDiopters?: number;
|
|
49
|
+
/** **Camera rig.** The FULL vertical angle, in RADIANS (three's `camera.fov` is degrees). */
|
|
50
|
+
verticalFov?: number;
|
|
51
|
+
/** **Camera rig.** Metres → world units on the eye; `0`/unset means 1. */
|
|
52
|
+
metersToVirtual?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The panel's hardware display state, as reported by `hardwaredisplaystatechange`.
|
|
57
|
+
*
|
|
58
|
+
* There is no page-facing request for it: it is a CONSEQUENCE of the active rendering mode —
|
|
59
|
+
* a `viewCount === 1` mode puts the panel flat, the 2-view mode puts it back.
|
|
60
|
+
*/
|
|
61
|
+
export type XRHardwareDisplayMode = '2d' | '3d';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What the glasses-free display physically is. Resolved by {@link TileHandle.getDisplayInfo};
|
|
65
|
+
* `null` there means the machine has no such display.
|
|
66
|
+
*
|
|
67
|
+
* `recommendedViewScaleX/Y` are **advisory**. The browser cannot resize a page's canvas, so
|
|
68
|
+
* nothing applies them for you — a page honours them by sizing its own backing store. Ignoring
|
|
69
|
+
* them costs sharpness or fill rate, never correctness.
|
|
70
|
+
*/
|
|
71
|
+
export interface XRDisplayInfo {
|
|
72
|
+
displayWidthMeters: number;
|
|
73
|
+
displayHeightMeters: number;
|
|
74
|
+
displayPixelWidth: number;
|
|
75
|
+
displayPixelHeight: number;
|
|
76
|
+
recommendedViewScaleX: number;
|
|
77
|
+
recommendedViewScaleY: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One rendering mode the DISPLAY can be put in, as reported by the runtime.
|
|
82
|
+
*
|
|
83
|
+
* The list is the display's, not the browser's. The DisplayXR Browser renders exactly **two**
|
|
84
|
+
* views — no view synthesis exists anywhere in the stack — so a mode needing MORE than two is
|
|
85
|
+
* reported with `isRequestable: false` and {@link TileHandle.requestRenderingMode} refuses it.
|
|
86
|
+
* Show such rows (they are what the panel can do) but mark them unavailable. A `viewCount === 1`
|
|
87
|
+
* mode IS requestable: the browser still submits two views and the runtime still weaves them —
|
|
88
|
+
* it is the PANEL that goes flat, which is how a page reaches the 2D hardware state.
|
|
89
|
+
*/
|
|
90
|
+
export interface XRDisplayRenderingMode {
|
|
91
|
+
modeIndex: number;
|
|
92
|
+
name: string;
|
|
93
|
+
/** @deprecated the browser reports `name`; kept for pages written against the earlier build. */
|
|
94
|
+
modeName?: string;
|
|
95
|
+
viewCount: number;
|
|
96
|
+
/** Per-view render scale the runtime recommends for this mode — advisory, like the display's. */
|
|
97
|
+
viewScaleX: number;
|
|
98
|
+
viewScaleY: number;
|
|
99
|
+
tileColumns: number;
|
|
100
|
+
tileRows: number;
|
|
101
|
+
viewWidthPixels: number;
|
|
102
|
+
viewHeightPixels: number;
|
|
103
|
+
/** True for a glasses-free 3D mode; false for a flat one. */
|
|
104
|
+
hardwareDisplay3D: boolean;
|
|
105
|
+
isActive: boolean;
|
|
106
|
+
/** False when the browser cannot drive it — in practice, `viewCount > 2`. */
|
|
107
|
+
isRequestable: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A rendering mode went active. Re-emitted on the wall and on every tile handle. */
|
|
111
|
+
export interface RenderingModeChange {
|
|
112
|
+
type: 'renderingmodechange';
|
|
113
|
+
/** The mode now active, or -1 if the browser named none and the list could not be read. */
|
|
114
|
+
modeIndex: number;
|
|
115
|
+
/** Its view count — the thing the automatic rig collapse turns on. Null if unknown. */
|
|
116
|
+
viewCount: number | null;
|
|
117
|
+
/** The full mode row, when it could be read back. */
|
|
118
|
+
mode: XRDisplayRenderingMode | null;
|
|
119
|
+
/** The browser's own event payload, unreshaped. */
|
|
120
|
+
detail: unknown;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The panel's hardware display state changed. */
|
|
124
|
+
export interface HardwareDisplayStateChange {
|
|
125
|
+
type: 'hardwaredisplaystatechange';
|
|
126
|
+
state: XRHardwareDisplayMode | null;
|
|
127
|
+
detail: unknown;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type DisplayModeChange = RenderingModeChange | HardwareDisplayStateChange;
|
|
131
|
+
|
|
132
|
+
/** What a page may lift into the floating native viewer. `null` = no `XRDisplayLayer.undock`. */
|
|
133
|
+
export interface UndockCapabilities {
|
|
134
|
+
model: boolean;
|
|
135
|
+
splat: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface UndockOptions {
|
|
139
|
+
/** Absolute https URL (or http on loopback) of the asset the NATIVE viewer loads. */
|
|
140
|
+
src: string;
|
|
141
|
+
type: 'model' | 'splat';
|
|
142
|
+
/** Lighting the page rendered with, so the viewer can match it. */
|
|
143
|
+
env?: 'room' | 'studio' | 'sky' | 'none';
|
|
144
|
+
/** The angle the page opened the asset at, so the undocked view opens at the same one. */
|
|
145
|
+
pose?: { yaw: number; pitch?: number; zoom?: number };
|
|
146
|
+
/** The page's fit margin, when it overrides the default. */
|
|
147
|
+
margin?: number;
|
|
148
|
+
title?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface UndockHandle {
|
|
152
|
+
/**
|
|
153
|
+
* Resolves when the viewer exits. On the protocol FALLBACK it resolves immediately and
|
|
154
|
+
* `detached` is true — a protocol launch is fire-and-forget and the page never hears back.
|
|
155
|
+
*/
|
|
156
|
+
ended: Promise<void>;
|
|
157
|
+
viewer: 'model' | 'splat';
|
|
158
|
+
detached: boolean;
|
|
159
|
+
/** The `displayxr-view:` URL, on the fallback path only. */
|
|
160
|
+
url?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
16
163
|
/** Extra options for {@link Inline3D.addScene}. */
|
|
17
164
|
export interface SceneOptions extends TileOptions {
|
|
18
165
|
/**
|
|
@@ -21,6 +168,15 @@ export interface SceneOptions extends TileOptions {
|
|
|
21
168
|
* and render the reported views as-is. Halving it doubles how much of the window an object fills.
|
|
22
169
|
*/
|
|
23
170
|
virtualDisplayHeight?: number;
|
|
171
|
+
/**
|
|
172
|
+
* A full {@link XRViewRigInit} instead of the scalar height — a posed display rig, or a camera
|
|
173
|
+
* rig. **Supersedes `virtualDisplayHeight`** (which is one particular display rig); passing
|
|
174
|
+
* both warns once and the rig wins **where rigs are supported** — which is the one reason to
|
|
175
|
+
* pass the pair deliberately, since a browser without {@link inline3dViewRigSupported} then
|
|
176
|
+
* falls back to the height you named rather than to its own default. The window weaves either
|
|
177
|
+
* way. Replaceable per frame with {@link TileHandle.setViewRig}.
|
|
178
|
+
*/
|
|
179
|
+
viewRig?: XRViewRigInit;
|
|
24
180
|
/** Element whose visibility drives the lazy create/close lifecycle (defaults to the canvas). */
|
|
25
181
|
observe?: Element;
|
|
26
182
|
}
|
|
@@ -52,6 +208,78 @@ export interface TileHandle {
|
|
|
52
208
|
* @deprecated See {@link TileHandle.exclude} — no-op on browsers with draw-order occlusion.
|
|
53
209
|
*/
|
|
54
210
|
unexclude(el: Element): void;
|
|
211
|
+
/**
|
|
212
|
+
* Replace this window's view rig. Cheap enough to call every frame — a rig applies per-locate,
|
|
213
|
+
* so animating one means sending new values, not tweening anything.
|
|
214
|
+
*
|
|
215
|
+
* Returns whether the rig reached a **live** layer. `false` means it was stored and will build
|
|
216
|
+
* the next one (a window scrolled away in lazy mode), or that the browser has no rig support
|
|
217
|
+
* ({@link inline3dViewRigSupported}) — in which case it warns once and the window keeps
|
|
218
|
+
* weaving on the runtime's default display rig.
|
|
219
|
+
*
|
|
220
|
+
* **One frame of lag, by construction.** The browser locates views *before* the page's rAF, so
|
|
221
|
+
* a rig set during frame N drives the views delivered in frame N+1. Invisible for a slider or
|
|
222
|
+
* a settled camera; not for a camera that moves with the pointer — for that, send an
|
|
223
|
+
* identity-posed camera rig and parent your eye cameras under the app camera
|
|
224
|
+
* (`cameraRigFromCamera(THREE, cam, { attach: true })` + `EyeCamera.setLocalFromView`), so the
|
|
225
|
+
* scene graph supplies this frame's world pose with no lag at all.
|
|
226
|
+
*
|
|
227
|
+
* While a `viewCount === 1` mode is active the rig is stored **as given** and pushed **flat**
|
|
228
|
+
* (a copy with `ipdFactor`/`parallaxFactor` at 0), so a page driving a rig every frame cannot
|
|
229
|
+
* walk out of 2D, and the 2-view mode going active restores exactly this rig.
|
|
230
|
+
*/
|
|
231
|
+
setViewRig(rig: XRViewRigInit): boolean;
|
|
232
|
+
/**
|
|
233
|
+
* The panel this window weaves on, or `null` where there is no glasses-free display.
|
|
234
|
+
*
|
|
235
|
+
* Rejects with an `Error` on a browser without the display-mode API
|
|
236
|
+
* ({@link inline3dDisplayModesSupported}) or while this window has no live layer (lazy mode,
|
|
237
|
+
* tile off screen).
|
|
238
|
+
*/
|
|
239
|
+
getDisplayInfo(): Promise<XRDisplayInfo | null>;
|
|
240
|
+
/** Every rendering mode the display can be put in. See {@link XRDisplayRenderingMode}. */
|
|
241
|
+
getRenderingModes(): Promise<ReadonlyArray<XRDisplayRenderingMode>>;
|
|
242
|
+
/**
|
|
243
|
+
* Ask the runtime to switch the display to `modeIndex`. A thin pass-through — it resolves and
|
|
244
|
+
* rejects exactly as the browser does.
|
|
245
|
+
*
|
|
246
|
+
* Rejects with a `TypeError` for a mode whose `viewCount > 2` (the browser is fixed at two
|
|
247
|
+
* views) or an unknown index — the browser raises those synchronously, and this pass-through
|
|
248
|
+
* turns them into rejections so one `.catch()` covers every failure — and with a
|
|
249
|
+
* `NotSupportedError` `DOMException` when the request was not forwardable. Success is signalled
|
|
250
|
+
* by the session's `renderingmodechange` event, not by this promise.
|
|
251
|
+
*
|
|
252
|
+
* A `viewCount === 1` mode is requestable and is how a page goes flat.
|
|
253
|
+
*/
|
|
254
|
+
requestRenderingMode(modeIndex: number): Promise<void>;
|
|
255
|
+
/**
|
|
256
|
+
* SUGAR over {@link TileHandle.requestRenderingMode}: `false` requests the first mode with
|
|
257
|
+
* `viewCount === 1 && isRequestable`, `true` the first with `viewCount === 2 && isRequestable`.
|
|
258
|
+
* It never touches the hardware display state directly (there is no such call) and never
|
|
259
|
+
* touches your rig.
|
|
260
|
+
*
|
|
261
|
+
* **The rig collapse is not part of this call.** When a 1-view mode actually goes ACTIVE the
|
|
262
|
+
* SDK zeroes every window's `ipdFactor`/`parallaxFactor` on the way to the layer and restores
|
|
263
|
+
* them when a 2-view mode does — driven by `renderingmodechange`, so it happens however the
|
|
264
|
+
* mode changed, and a **refused request changes nothing in either direction**. The flattening
|
|
265
|
+
* is a copy pushed at the layer, never a write into your descriptor, so the restore is literally
|
|
266
|
+
* the rig you last set — and it survives a per-frame `setViewRig` loop, a lazy tile rebuilding
|
|
267
|
+
* its layer, and a window that never set a rig at all.
|
|
268
|
+
*
|
|
269
|
+
* Rejects with an `Error` when no such mode is listed, otherwise as `requestRenderingMode` does.
|
|
270
|
+
*/
|
|
271
|
+
setStereoEnabled(enabled: boolean): Promise<boolean>;
|
|
272
|
+
/** Listen for one display event, re-emitted on this handle. Returns an unsubscribe function. */
|
|
273
|
+
on(type: 'renderingmodechange', cb: (e: RenderingModeChange) => void): () => void;
|
|
274
|
+
on(type: 'hardwaredisplaystatechange', cb: (e: HardwareDisplayStateChange) => void): () => void;
|
|
275
|
+
/** Drop a listener registered with {@link TileHandle.on}. */
|
|
276
|
+
off(type: 'renderingmodechange', cb: (e: RenderingModeChange) => void): void;
|
|
277
|
+
off(type: 'hardwaredisplaystatechange', cb: (e: HardwareDisplayStateChange) => void): void;
|
|
278
|
+
/**
|
|
279
|
+
* Both display events through one callback — the older shape, still supported. Returns an
|
|
280
|
+
* unsubscribe function; inert (a no-op unsubscribe) on a browser without the API.
|
|
281
|
+
*/
|
|
282
|
+
onDisplayModeChange(cb: (e: DisplayModeChange) => void): () => void;
|
|
55
283
|
/**
|
|
56
284
|
* Per-window frame counters, for diagnosing the load-induced mono fallback.
|
|
57
285
|
*
|
|
@@ -76,6 +304,37 @@ export interface Inline3D {
|
|
|
76
304
|
/** Number of currently-active (weaving) windows. */
|
|
77
305
|
readonly liveCount: number;
|
|
78
306
|
|
|
307
|
+
// The panel is the DOCUMENT's, not a tile's, so the display API lives here; the same names are
|
|
308
|
+
// on every tile handle, routed to whichever window currently holds a live layer.
|
|
309
|
+
|
|
310
|
+
/** The panel, or `null` where there is no glasses-free display. See {@link TileHandle.getDisplayInfo}. */
|
|
311
|
+
getDisplayInfo(): Promise<XRDisplayInfo | null>;
|
|
312
|
+
/** Every rendering mode the display can be put in. See {@link XRDisplayRenderingMode}. */
|
|
313
|
+
getRenderingModes(): Promise<ReadonlyArray<XRDisplayRenderingMode>>;
|
|
314
|
+
/** Switch the display to `modeIndex`. See {@link TileHandle.requestRenderingMode}. */
|
|
315
|
+
requestRenderingMode(modeIndex: number): Promise<void>;
|
|
316
|
+
/** Sugar: `false` -> a 1-view mode, `true` -> the 2-view mode. See {@link TileHandle.setStereoEnabled}. */
|
|
317
|
+
setStereoEnabled(enabled: boolean): Promise<boolean>;
|
|
318
|
+
on(type: 'renderingmodechange', cb: (e: RenderingModeChange) => void): () => void;
|
|
319
|
+
on(type: 'hardwaredisplaystatechange', cb: (e: HardwareDisplayStateChange) => void): () => void;
|
|
320
|
+
off(type: 'renderingmodechange', cb: (e: RenderingModeChange) => void): void;
|
|
321
|
+
off(type: 'hardwaredisplaystatechange', cb: (e: HardwareDisplayStateChange) => void): void;
|
|
322
|
+
/** As last REPORTED by `hardwaredisplaystatechange` — never what was last requested. */
|
|
323
|
+
readonly hardwareDisplayState: XRHardwareDisplayMode | null;
|
|
324
|
+
/** The active mode as last read/reported. `viewCount: 0` means "not read yet". */
|
|
325
|
+
readonly activeMode: { modeIndex: number; viewCount: number };
|
|
326
|
+
/** True while the SDK is holding every window's rig flat because a 1-view mode is active. */
|
|
327
|
+
readonly stereoCollapsed: boolean;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* What this build can lift into the floating native viewer, or `null` on a browser with no
|
|
331
|
+
* `XRDisplayLayer.undock` — that null is what a page branches on. Read off the first live
|
|
332
|
+
* layer (`layer.getUndockCapabilities()`); `{model:false, splat:false}` is the pre-read value.
|
|
333
|
+
*/
|
|
334
|
+
readonly undock: UndockCapabilities | null;
|
|
335
|
+
/** Re-read {@link Inline3D.undock} off a live layer. */
|
|
336
|
+
refreshUndock(): Promise<UndockCapabilities | null>;
|
|
337
|
+
|
|
79
338
|
/** Weave a still side-by-side 3D photo from a URL or decoded image source. */
|
|
80
339
|
addImage(
|
|
81
340
|
canvas: HTMLCanvasElement,
|
|
@@ -194,6 +453,61 @@ export function inline3dOverlaySupported(): boolean;
|
|
|
194
453
|
*/
|
|
195
454
|
export function inline3dOcclusionByDrawOrder(): boolean;
|
|
196
455
|
|
|
456
|
+
/**
|
|
457
|
+
* True when this browser accepts a full {@link XRViewRigInit} — {@link TileHandle.setViewRig} and
|
|
458
|
+
* {@link SceneOptions.viewRig}, i.e. a posed display rig or a camera rig, instead of only the
|
|
459
|
+
* scalar `virtualDisplayHeight`. Sync + cheap; implies {@link inline3DAvailable}.
|
|
460
|
+
*
|
|
461
|
+
* Reads a capability (the presence of `XRDisplayLayer.setViewRig`), never a version or UA string,
|
|
462
|
+
* and is `false` on every browser that predates the rig API — where `virtualDisplayHeight` still
|
|
463
|
+
* works. Branch on it only if a camera rig is load-bearing for your page: `setViewRig` no-ops
|
|
464
|
+
* (warning once) rather than throwing, so a page that merely wants the extra control where it
|
|
465
|
+
* exists can call it unconditionally.
|
|
466
|
+
*/
|
|
467
|
+
export function inline3dViewRigSupported(): boolean;
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* True when this browser exposes the DISPLAY-MODE API — {@link TileHandle.getDisplayInfo},
|
|
471
|
+
* {@link TileHandle.getRenderingModes} and {@link TileHandle.requestRenderingMode}. Sync + cheap;
|
|
472
|
+
* implies {@link inline3DAvailable}.
|
|
473
|
+
*
|
|
474
|
+
* Reads a capability (all three methods present on `XRDisplayLayer.prototype`), never a version or
|
|
475
|
+
* UA string, and demands all three: a browser shipping half the set is one mid-implementation.
|
|
476
|
+
* Everything the API drives is optional enhancement, so branch on this only to decide whether to
|
|
477
|
+
* show display controls — the handle methods reject with a clear `Error` rather than throwing at
|
|
478
|
+
* import or create time.
|
|
479
|
+
*/
|
|
480
|
+
export function inline3dDisplayModesSupported(): boolean;
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* True when this browser can undock through `XRDisplayLayer.undock()` — i.e. without the
|
|
484
|
+
* `displayxr-view:` protocol prompt the fallback needs. A page does not have to branch on it to
|
|
485
|
+
* undock (the helper falls back on its own); it is the probe for whether {@link Inline3D.undock}
|
|
486
|
+
* carries capabilities.
|
|
487
|
+
*/
|
|
488
|
+
export function inline3dUndockSupported(): boolean;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Undock `target`'s asset into the floating native viewer over the desktop.
|
|
492
|
+
*
|
|
493
|
+
* **Call it synchronously inside the click.** Both paths need the transient user activation — the
|
|
494
|
+
* API path to be allowed at all, the fallback to get Chrome's protocol dialog — and an `await`
|
|
495
|
+
* before this call spends it.
|
|
496
|
+
*
|
|
497
|
+
* Rejects with an `Error` whose `name` is `'not-installed'`, `'src-not-allowed'`,
|
|
498
|
+
* `'no-activation'` or `'busy'`.
|
|
499
|
+
*/
|
|
500
|
+
export function undock(target: Element, opts: UndockOptions): Promise<UndockHandle>;
|
|
501
|
+
|
|
502
|
+
/** True where a native DisplayXR viewer can exist at all (the viewers are Windows-only today). */
|
|
503
|
+
export function undockAvailable(): boolean;
|
|
504
|
+
|
|
505
|
+
/** The `displayxr-view:` URL the fallback path navigates to — exported for logging and tests. */
|
|
506
|
+
export function undockUrl(el: Element, opts: UndockOptions): string;
|
|
507
|
+
|
|
508
|
+
/** An element's rect in physical screen pixels — where the native viewer places its window. */
|
|
509
|
+
export function tileScreenRect(el: Element): { x: number; y: number; w: number; h: number; dpr: number };
|
|
510
|
+
|
|
197
511
|
/** Open the page's inline-3D session and return a manager you add windows to. */
|
|
198
512
|
export function createInline3D(
|
|
199
513
|
opts?: CreateInline3DOptions,
|
|
@@ -229,5 +543,26 @@ export interface XRDisplayLayer {
|
|
|
229
543
|
* browser shipped so far — the SDK treats absent as `false` and runs the legacy path.
|
|
230
544
|
*/
|
|
231
545
|
readonly occlusionByDrawOrder?: boolean;
|
|
546
|
+
/**
|
|
547
|
+
* Replace the rig the runtime locates this layer's views against. Optional because it is
|
|
548
|
+
* absent on browsers that predate the rig API — its PRESENCE on the prototype is the
|
|
549
|
+
* capability signal ({@link inline3dViewRigSupported}), which is why the browser exposes it as
|
|
550
|
+
* a method: a Blink IDL attribute getter throws `Illegal invocation` when read off a prototype
|
|
551
|
+
* (see {@link XRDisplayLayer.occlusionByDrawOrder}), so an attribute could not be probed at
|
|
552
|
+
* all on the browser that has it.
|
|
553
|
+
*/
|
|
554
|
+
setViewRig?(rig: XRViewRigInit): void;
|
|
555
|
+
/**
|
|
556
|
+
* The display-mode API. All three optional for the same reason as `setViewRig`: their presence
|
|
557
|
+
* on the prototype IS the capability signal ({@link inline3dDisplayModesSupported}), and the
|
|
558
|
+
* SDK demands all three before treating the browser as supporting any of them.
|
|
559
|
+
*/
|
|
560
|
+
getDisplayInfo?(): Promise<XRDisplayInfo | null>;
|
|
561
|
+
getRenderingModes?(): Promise<ReadonlyArray<XRDisplayRenderingMode>>;
|
|
562
|
+
/** Throws `TypeError` **synchronously** for `viewCount > 2` or an unknown index. */
|
|
563
|
+
requestRenderingMode?(modeIndex: number): Promise<void>;
|
|
564
|
+
/** Undock this layer's asset into the floating native viewer. Optional; same probe rule. */
|
|
565
|
+
undock?(init: UndockOptions): Promise<unknown>;
|
|
566
|
+
getUndockCapabilities?(): Promise<UndockCapabilities>;
|
|
232
567
|
close(): void;
|
|
233
568
|
}
|
package/js/inline3d-model.js
CHANGED
|
@@ -478,7 +478,7 @@ function boundsOf(object3d) {
|
|
|
478
478
|
* This is the default because the alternative is silently wrong. `addStudioLights` is punctual
|
|
479
479
|
* only, and a punctual light contributes a specular highlight without filling a metallic BRDF —
|
|
480
480
|
* so a `metalness: 1` surface has nothing to reflect and resolves to BLACK. Chrome bells render as
|
|
481
|
-
* a dark disc, glass
|
|
481
|
+
* a dark disc, clear-glass optics as opaque holes, and the result reads as a corrupt asset rather than a
|
|
482
482
|
* lighting choice. It has cost real debugging time more than once.
|
|
483
483
|
*
|
|
484
484
|
* RoomEnvironment is generated in memory — a small box of emissive panels — so this buys IBL with
|