@displayxr/inline3d 1.5.0 → 1.5.1
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 +13 -0
- package/js/inline3d-undock.js +55 -27
- package/js/inline3d.js +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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.5.1 — 2026-09-06
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Undock: `ended` now means the viewer exited.** The browser contract (browser-pvt#25) resolves
|
|
13
|
+
`layer.undock()` on a successful LAUNCH and reports the viewer's exit separately as the XRSession's
|
|
14
|
+
`undockend` event; 1.4.0/1.5.0 derived `ended` from the launch promise, so it resolved a frame after
|
|
15
|
+
the window opened. The helper now arms an `undockend` listener before launching and resolves
|
|
16
|
+
`ended` on it; the browser's DOMException names (NotAllowedError, NotSupportedError,
|
|
17
|
+
SecurityError, InvalidStateError, OperationError) map onto the four contract names. The
|
|
18
|
+
fallback path is unchanged (`detached === true`, `ended` immediate).
|
|
19
|
+
- README: vendoring note — `inline3d-mode-switch.js` is a static dependency of `inline3d.js`.
|
|
20
|
+
|
|
8
21
|
## 1.5.0 — 2026-09-06
|
|
9
22
|
|
|
10
23
|
### Added
|
package/js/inline3d-undock.js
CHANGED
|
@@ -135,9 +135,19 @@ function launchProtocol(url) {
|
|
|
135
135
|
|
|
136
136
|
const UNDOCK_ERRORS = ['not-installed', 'src-not-allowed', 'no-activation', 'busy'];
|
|
137
137
|
|
|
138
|
+
// The browser refuses with DOMException names (patch 0130); each maps onto one contract name.
|
|
139
|
+
const DOM_ERROR_NAMES = {
|
|
140
|
+
NotAllowedError: 'no-activation', // no transient user activation
|
|
141
|
+
NotSupportedError: 'not-installed', // no registered viewer for this type
|
|
142
|
+
SecurityError: 'src-not-allowed', // src outside the allowlist
|
|
143
|
+
InvalidStateError: 'busy', // an undock is already live in this frame
|
|
144
|
+
OperationError: 'not-installed', // the viewer failed to launch
|
|
145
|
+
};
|
|
146
|
+
|
|
138
147
|
/** Give a rejection one of the four contract names, keeping the browser's own where it has one. */
|
|
139
148
|
function undockError(e, fallbackName, message) {
|
|
140
|
-
const name =
|
|
149
|
+
const name =
|
|
150
|
+
e && UNDOCK_ERRORS.includes(e.name) ? e.name : (e && DOM_ERROR_NAMES[e.name]) || fallbackName;
|
|
141
151
|
const err = new Error(message || (e && e.message) || `[inline3d] undock failed (${name}).`);
|
|
142
152
|
err.name = name;
|
|
143
153
|
if (e) err.cause = e;
|
|
@@ -166,8 +176,10 @@ let inFlight = false;
|
|
|
166
176
|
* @param {number} [opts.margin] the page's fit margin, when it overrides the default.
|
|
167
177
|
* @param {string} [opts.title]
|
|
168
178
|
* @returns {Promise<{ended:Promise<void>, viewer:UndockType, detached?:boolean}>}
|
|
169
|
-
*
|
|
170
|
-
*
|
|
179
|
+
* Resolves once the viewer has LAUNCHED (API path: `layer.undock()` resolved, i.e. the viewer
|
|
180
|
+
* process was spawned; it never waits for the viewer). `ended` resolves when the viewer exits -
|
|
181
|
+
* the API path hears that as the XRSession's `undockend` event; the fallback path never hears
|
|
182
|
+
* back, so there `ended` resolves immediately and `detached === true`.
|
|
171
183
|
* Rejects with an Error named `not-installed` | `src-not-allowed` | `no-activation` | `busy`.
|
|
172
184
|
*/
|
|
173
185
|
export function undock(target, opts) {
|
|
@@ -184,7 +196,11 @@ export function undock(target, opts) {
|
|
|
184
196
|
}
|
|
185
197
|
if (inFlight) return Promise.reject(undockError(null, 'busy', '[inline3d] an undock is already in flight.'));
|
|
186
198
|
|
|
187
|
-
|
|
199
|
+
// The resolver hands back `{layer, session}` (the session carries the `undockend` event); a bare
|
|
200
|
+
// layer is accepted too, in which case the viewer's exit is simply not observable.
|
|
201
|
+
const found = layerResolver ? layerResolver(target) : null;
|
|
202
|
+
const layer = found && typeof found === 'object' && 'layer' in found ? found.layer : found;
|
|
203
|
+
const session = found && typeof found === 'object' && 'session' in found ? found.session : null;
|
|
188
204
|
const viewer = opts.type;
|
|
189
205
|
|
|
190
206
|
// ── fallback: the OS protocol ──────────────────────────────────────────────────────────
|
|
@@ -211,41 +227,53 @@ export function undock(target, opts) {
|
|
|
211
227
|
if (opts.margin !== undefined) init.margin = opts.margin;
|
|
212
228
|
if (opts.title) init.title = opts.title;
|
|
213
229
|
|
|
230
|
+
// THE BROWSER CONTRACT (browser-pvt#25 / patch 0130): `layer.undock(init)` RESOLVES ON A
|
|
231
|
+
// SUCCESSFUL LAUNCH - as soon as the viewer process is spawned - and never waits for it; every
|
|
232
|
+
// refusal is a prompt rejection (NotAllowedError / NotSupportedError / SecurityError /
|
|
233
|
+
// InvalidStateError / OperationError). The viewer's exit arrives separately, as the `undockend`
|
|
234
|
+
// event on the XRSession. One live undock per frame, so the NEXT `undockend` after a
|
|
235
|
+
// successful launch is this one's - no correlation id needed. The listener is armed BEFORE the
|
|
236
|
+
// launch so a viewer that exits immediately cannot slip between the two.
|
|
237
|
+
let endedResolve = null;
|
|
238
|
+
const ended = new Promise((resolve) => {
|
|
239
|
+
if (!session || typeof session.addEventListener !== 'function') {
|
|
240
|
+
// No session to listen on: the launch still works, the exit is simply not observable -
|
|
241
|
+
// so `ended` resolves at launch (as the fallback path does) rather than holding the
|
|
242
|
+
// one-live-undock guard for ever.
|
|
243
|
+
endedResolve = resolve;
|
|
244
|
+
resolve();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const onEnd = () => {
|
|
248
|
+
session.removeEventListener('undockend', onEnd);
|
|
249
|
+
resolve();
|
|
250
|
+
};
|
|
251
|
+
session.addEventListener('undockend', onEnd);
|
|
252
|
+
endedResolve = () => {
|
|
253
|
+
session.removeEventListener('undockend', onEnd);
|
|
254
|
+
resolve();
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
|
|
214
258
|
let call;
|
|
215
259
|
try {
|
|
216
260
|
call = Promise.resolve(layer.undock(init));
|
|
217
261
|
} catch (e) {
|
|
218
262
|
// A synchronous throw is the same failure as a rejection; one .catch() should cover both.
|
|
263
|
+
if (endedResolve) endedResolve();
|
|
219
264
|
return Promise.reject(undockError(e, 'src-not-allowed'));
|
|
220
265
|
}
|
|
221
266
|
inFlight = true;
|
|
222
|
-
|
|
267
|
+
ended.then(() => {
|
|
223
268
|
inFlight = false;
|
|
224
269
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// (not-installed / src-not-allowed / no-activation / busy) is decided before any window
|
|
228
|
-
// exists — or it stays pending until the viewer exits. A short race tells them apart without
|
|
229
|
-
// inventing an event: whatever has not rejected by then launched. The launch has already
|
|
230
|
-
// happened synchronously above, so this wait costs the user nothing.
|
|
231
|
-
const LAUNCH_MS = 150;
|
|
232
|
-
const launchProbe = settled.then(
|
|
233
|
-
() => 'launched',
|
|
270
|
+
return call.then(
|
|
271
|
+
() => ({ ended, viewer, detached: false }),
|
|
234
272
|
(e) => {
|
|
273
|
+
// Refused before any window existed: nothing is in flight and nothing will end.
|
|
274
|
+
inFlight = false;
|
|
275
|
+
if (endedResolve) endedResolve();
|
|
235
276
|
throw undockError(e, 'not-installed');
|
|
236
277
|
}
|
|
237
278
|
);
|
|
238
|
-
// The probe LOSES the race whenever the viewer stays open, and a rejection arriving after that
|
|
239
|
-
// would otherwise be an unhandled one. Marked handled here; the same rejection still reaches
|
|
240
|
-
// the caller through `ended`, which is where a late failure belongs.
|
|
241
|
-
launchProbe.catch(() => {});
|
|
242
|
-
return Promise.race([
|
|
243
|
-
launchProbe,
|
|
244
|
-
new Promise((resolve) => window.setTimeout(() => resolve('launched'), LAUNCH_MS)),
|
|
245
|
-
]).then(() => ({
|
|
246
|
-
// A rejection AFTER the launch window is the viewer failing later, and it belongs on `ended`.
|
|
247
|
-
ended: settled.then(() => undefined),
|
|
248
|
-
viewer,
|
|
249
|
-
detached: false,
|
|
250
|
-
}));
|
|
251
279
|
}
|
package/js/inline3d.js
CHANGED
|
@@ -59,13 +59,15 @@ setUndockLayerResolver((el) => {
|
|
|
59
59
|
// The canvas itself, then a woven canvas INSIDE the element (a card wrapping its tile), then
|
|
60
60
|
// the element sitting inside a window's own container (a button in the tile's box). Anything
|
|
61
61
|
// further away is not this window's rect and takes the fallback.
|
|
62
|
-
|
|
62
|
+
// The session rides along: the viewer's exit is the XRSession's `undockend` event.
|
|
63
|
+
const hit = (win) => ({ layer: win.layer, session: m.session });
|
|
64
|
+
for (const win of m._windows.values()) if (win.canvas === el && win.layer) return hit(win);
|
|
63
65
|
for (const win of m._windows.values()) {
|
|
64
|
-
if (win.layer && typeof el.contains === 'function' && el.contains(win.canvas)) return win
|
|
66
|
+
if (win.layer && typeof el.contains === 'function' && el.contains(win.canvas)) return hit(win);
|
|
65
67
|
}
|
|
66
68
|
for (const win of m._windows.values()) {
|
|
67
69
|
const box = win.canvas.parentElement;
|
|
68
|
-
if (win.layer && box && typeof box.contains === 'function' && box.contains(el)) return win
|
|
70
|
+
if (win.layer && box && typeof box.contains === 'function' && box.contains(el)) return hit(win);
|
|
69
71
|
}
|
|
70
72
|
return null;
|
|
71
73
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a glasses-free-3D window on a DisplayXR display, inside an ordinary web page. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|