@needle-tools/engine 5.1.0 → 5.1.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 +1 -1
- package/dist/{needle-engine.bundle-DAAfj8Cs.min.js → needle-engine.bundle-BUT4ua7U.min.js} +145 -145
- package/dist/{needle-engine.bundle-DvOrfXws.umd.cjs → needle-engine.bundle-Cb39lTJn.umd.cjs} +144 -144
- package/dist/{needle-engine.bundle-5oIL49YP.js → needle-engine.bundle-Dv-p5m4z.js} +5774 -5741
- package/dist/needle-engine.d.ts +11 -5
- package/dist/needle-engine.js +191 -191
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +72 -72
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_networking_peer.js +19 -13
- package/lib/engine/engine_networking_peer.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/webcomponents/WebXRButtons.js +5 -6
- package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.d.ts +7 -1
- package/lib/engine/xr/NeedleXRSession.js +70 -11
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/webxr/WebXR.js +25 -14
- package/lib/engine-components/webxr/WebXR.js.map +1 -1
- package/package.json +2 -2
- package/plugins/common/license.js +4 -4
- package/plugins/vite/license.js +4 -4
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +69 -69
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_networking_peer.ts +12 -13
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/WebXRButtons.ts +5 -6
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/NeedleXRSession.ts +72 -14
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/webxr/WebXR.ts +23 -15
|
@@ -23,6 +23,28 @@ import type { IXRRig } from "./XRRig.js";
|
|
|
23
23
|
const measure_SessionStartedMarker = "NeedleXRSession onStart";
|
|
24
24
|
const measure_SessionEndedMarker = "NeedleXRSession onEnd";
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Whether the `xr-spatial-tracking` permission policy is allowed in the current document.
|
|
28
|
+
* WebXR APIs (isSessionSupported / requestSession) require it; an embedded page/iframe must
|
|
29
|
+
* be granted it via the iframe `allow` attribute (delegated from a parent that has it) and/or
|
|
30
|
+
* a `Permissions-Policy` response header. Returns true when the policy API is unavailable
|
|
31
|
+
* (can't determine → don't block, preserve prior behavior).
|
|
32
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#allow
|
|
33
|
+
*/
|
|
34
|
+
function isXrSpatialTrackingAllowed(): boolean {
|
|
35
|
+
try {
|
|
36
|
+
const doc = globalThis.document as (Document & { permissionsPolicy?: { allowsFeature(f: string): boolean }, featurePolicy?: { allowsFeature(f: string): boolean } }) | undefined;
|
|
37
|
+
const policy = doc?.permissionsPolicy ?? doc?.featurePolicy;
|
|
38
|
+
if (policy && typeof policy.allowsFeature === "function") {
|
|
39
|
+
return policy.allowsFeature("xr-spatial-tracking");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* ignore — fall through to default */
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
|
|
27
49
|
/** @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame/fillPoses */
|
|
28
50
|
declare type FillPosesFunction = (spaces: IterableIterator<XRJointSpace>, referenceSpace: XRSpace, targetArray: Float32Array) => void;
|
|
@@ -85,6 +107,10 @@ async function handleSessionGranted() {
|
|
|
85
107
|
let defaultMode: XRSessionMode = "immersive-vr";
|
|
86
108
|
|
|
87
109
|
try {
|
|
110
|
+
// If xr-spatial-tracking isn't allowed in this document (e.g. an embed whose page/iframe
|
|
111
|
+
// wasn't granted it), XR can't run here — skip the probe so we don't log a
|
|
112
|
+
// Permissions-Policy violation on load.
|
|
113
|
+
if (!isXrSpatialTrackingAllowed()) return;
|
|
88
114
|
// In app clips we default to AR
|
|
89
115
|
if (DeviceUtilities.isNeedleAppClip()) {
|
|
90
116
|
defaultMode = "immersive-ar";
|
|
@@ -304,7 +330,9 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
304
330
|
*
|
|
305
331
|
* **IMPORTANT**: the resulting URL must exactly match an App Clip experience URL you have registered (with custom card metadata).
|
|
306
332
|
* For this reason the URL is opened as-is, without appending any query parameters.
|
|
307
|
-
*
|
|
333
|
+
*
|
|
334
|
+
* Note: custom App Clip branding requires an active PRO subscription. See the iOS WebXR App Clip docs (Custom Branding for iOS AR).
|
|
335
|
+
* @link https://engine.needle.tools/docs/how-to-guides/xr/ios-webxr-app-clip#custom-branding-for-ios-ar
|
|
308
336
|
*/
|
|
309
337
|
static get appClipUrl(): string | null { return this._appClipUrl; }
|
|
310
338
|
static set appClipUrl(value: string | null) {
|
|
@@ -367,19 +395,49 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
367
395
|
* @param mode The XRSessionMode to check if it is supported
|
|
368
396
|
* @returns true if the browser supports the given XRSessionMode
|
|
369
397
|
*/
|
|
370
|
-
static isSessionSupported(mode: XRSessionMode) {
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
398
|
+
static isSessionSupported(mode: XRSessionMode): Promise<boolean> {
|
|
399
|
+
// Re-probe when XR devices change (e.g. a headset connects).
|
|
400
|
+
this.ensureDeviceChangeInvalidation();
|
|
401
|
+
|
|
402
|
+
// Cache AND de-duplicate the check by storing the in-flight PROMISE (not just the
|
|
403
|
+
// resolved value). A burst of callers in the same tick (XR buttons, offer-session,
|
|
404
|
+
// support checks) then shares ONE underlying navigator.xr.isSessionSupported() call
|
|
405
|
+
// per mode. Each call otherwise repeats the `xr-spatial-tracking` Permissions-Policy
|
|
406
|
+
// check and floods the console. It also keeps user-input onclick checks fast.
|
|
407
|
+
const cached = this._sessionSupportedCache[mode];
|
|
408
|
+
if (cached !== undefined) return cached;
|
|
409
|
+
|
|
410
|
+
let result: Promise<boolean>;
|
|
411
|
+
// isSessionSupported() requires the `xr-spatial-tracking` permission policy to be
|
|
412
|
+
// allowed in this document. In an embed whose page/iframe wasn't granted it, calling
|
|
413
|
+
// the API logs a Permissions-Policy violation and can't succeed — so skip the call
|
|
414
|
+
// and report "unsupported" instead of spamming the console.
|
|
415
|
+
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#allow
|
|
416
|
+
if (!isXrSpatialTrackingAllowed()) {
|
|
417
|
+
if (debug) console.warn("[WebXR] xr-spatial-tracking is not allowed in this document — reporting XR as unsupported. The embedding page/iframe must grant the policy.");
|
|
418
|
+
result = Promise.resolve(false);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
result = (this.xrSystem?.isSessionSupported(mode) ?? Promise.resolve(false))
|
|
422
|
+
.catch(err => { if (debug) console.error(err); return false; });
|
|
423
|
+
}
|
|
424
|
+
this._sessionSupportedCache[mode] = result;
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private static _sessionSupportedCache: { [mode in XRSessionMode]?: Promise<boolean> } = {};
|
|
429
|
+
private static _deviceChangeHooked = false;
|
|
430
|
+
/** Invalidate the support cache when the set of XR devices changes, so a newly
|
|
431
|
+
* connected headset is picked up by the next isSessionSupported() call. */
|
|
432
|
+
private static ensureDeviceChangeInvalidation() {
|
|
433
|
+
if (this._deviceChangeHooked) return;
|
|
434
|
+
this._deviceChangeHooked = true;
|
|
435
|
+
try {
|
|
436
|
+
this.xrSystem?.addEventListener?.("devicechange", () => { this._sessionSupportedCache = {}; });
|
|
437
|
+
}
|
|
438
|
+
catch { /* xrSystem may not support events; cache simply won't be invalidated */ }
|
|
379
439
|
}
|
|
380
440
|
|
|
381
|
-
private static _sessionSupportedCache: { [mode in XRSessionMode]?: boolean } = {};
|
|
382
|
-
|
|
383
441
|
private static _currentSessionRequest?: Promise<XRSession>;
|
|
384
442
|
private static _activeSession: NeedleXRSession | null;
|
|
385
443
|
|
|
@@ -650,7 +708,7 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
650
708
|
// Setup VR initialization parameters
|
|
651
709
|
case "immersive-ar":
|
|
652
710
|
{
|
|
653
|
-
const supported = await
|
|
711
|
+
const supported = await NeedleXRSession.isSessionSupported('immersive-ar')
|
|
654
712
|
if (supported !== true) {
|
|
655
713
|
console.error(mode + ' is not supported by this browser.');
|
|
656
714
|
return null;
|
|
@@ -671,7 +729,7 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
671
729
|
// Setup AR initialization parameters
|
|
672
730
|
case "immersive-vr":
|
|
673
731
|
{
|
|
674
|
-
const supported = await
|
|
732
|
+
const supported = await NeedleXRSession.isSessionSupported('immersive-vr')
|
|
675
733
|
if (supported !== true) {
|
|
676
734
|
console.error(mode + ' is not supported by this browser.');
|
|
677
735
|
return null;
|
|
@@ -3,7 +3,7 @@ import { ArrayCamera, AxesHelper, Camera, Color, DirectionalLight, Fog, GridHelp
|
|
|
3
3
|
import { needleLogoOnlySVG } from "../assets/index.js";
|
|
4
4
|
import { isDevEnvironment } from "../debug/index.js";
|
|
5
5
|
import { ObjectUtils, PrimitiveType } from "../engine_create_objects.js";
|
|
6
|
-
import { $
|
|
6
|
+
import { $Mhcqc } from "../engine_license.js";
|
|
7
7
|
import { Mathf } from "../engine_math.js";
|
|
8
8
|
import { delay, DeviceUtilities } from "../engine_utils.js";
|
|
9
9
|
|
|
@@ -215,7 +215,7 @@ export class TemporaryXRContext {
|
|
|
215
215
|
this._scene.background = new Color(0x000000);
|
|
216
216
|
|
|
217
217
|
let logoSrc = needleLogoOnlySVG;
|
|
218
|
-
if ($
|
|
218
|
+
if ($Mhcqc()) {
|
|
219
219
|
const htmlComponent = document.querySelector("needle-engine");
|
|
220
220
|
if (htmlComponent) {
|
|
221
221
|
const licenseLogo = htmlComponent.getAttribute("logo-src");
|
|
@@ -3,7 +3,7 @@ import { Euler, Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "t
|
|
|
3
3
|
|
|
4
4
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
|
|
5
5
|
import { findObjectOfType } from "../../../engine/engine_components.js";
|
|
6
|
-
import {
|
|
6
|
+
import { __dSkNu } from "../../../engine/engine_license.js";
|
|
7
7
|
import { serializable } from "../../../engine/engine_serialization.js";
|
|
8
8
|
import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
|
|
9
9
|
import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js";
|
|
@@ -277,7 +277,7 @@ export class USDZExporter extends Behaviour {
|
|
|
277
277
|
let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
|
|
278
278
|
name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
|
|
279
279
|
|
|
280
|
-
if (!
|
|
280
|
+
if (!__dSkNu()) {
|
|
281
281
|
if (name !== "") name += "-";
|
|
282
282
|
name += "MadeWithNeedle";
|
|
283
283
|
}
|
|
@@ -682,7 +682,7 @@ export class USDZExporter extends Behaviour {
|
|
|
682
682
|
if (debug)
|
|
683
683
|
showBalloonMessage("Quicklook url: " + callToActionURL);
|
|
684
684
|
if (callToActionURL) {
|
|
685
|
-
if (!
|
|
685
|
+
if (!__dSkNu()) {
|
|
686
686
|
console.warn("Quicklook closed: custom redirects require a Needle Engine Pro license: https://needle.tools/pricing", callToActionURL)
|
|
687
687
|
}
|
|
688
688
|
else {
|
|
@@ -697,7 +697,7 @@ export class USDZExporter extends Behaviour {
|
|
|
697
697
|
private buildQuicklookOverlay(): CustomBranding {
|
|
698
698
|
const obj: CustomBranding = {};
|
|
699
699
|
if (this.customBranding) Object.assign(obj, this.customBranding);
|
|
700
|
-
if (!
|
|
700
|
+
if (!__dSkNu()) {
|
|
701
701
|
console.log("Custom Quicklook banner text requires pro license: https://needle.tools/pricing");
|
|
702
702
|
obj.callToAction = "Close";
|
|
703
703
|
obj.checkoutTitle = "🌵 Made with Needle";
|
|
@@ -237,22 +237,30 @@ export class WebXR extends Behaviour {
|
|
|
237
237
|
// Showing the QuickLook button depends on whether we're on iOS or visionOS –
|
|
238
238
|
// on iOS we have AppClip support, so we don't need QuickLook button there,
|
|
239
239
|
// while on visionOS we use QuickLook for AR experiences for now (unless it happens to have WebXR support by then).
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
|
|
250
|
-
this._usdzExporter.objectToExport = this.context.scene;
|
|
251
|
-
this._usdzExporter.autoExportAnimations = true;
|
|
252
|
-
this._usdzExporter.autoExportAudioSources = true;
|
|
253
|
-
}
|
|
240
|
+
const addUsdzExporterIfMissing = () => {
|
|
241
|
+
const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
|
|
242
|
+
if (!existingUSDZExporter) {
|
|
243
|
+
// if no USDZ Exporter is found we add one and assign the scene to be exported
|
|
244
|
+
if (debug) console.log("WebXR: Adding USDZExporter");
|
|
245
|
+
this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
|
|
246
|
+
this._usdzExporter.objectToExport = this.context.scene;
|
|
247
|
+
this._usdzExporter.autoExportAnimations = true;
|
|
248
|
+
this._usdzExporter.autoExportAudioSources = true;
|
|
254
249
|
}
|
|
255
|
-
}
|
|
250
|
+
};
|
|
251
|
+
// QuickLook is added when explicitly requested, or as the visionOS fallback when
|
|
252
|
+
// WebXR AR isn't available. Only probe XR support on visionOS (the only case that
|
|
253
|
+
// consumes the result) and go through the cached NeedleXRSession.isARSupported() so
|
|
254
|
+
// we don't eagerly hit navigator.xr on every enable — that probe trips the
|
|
255
|
+
// `xr-spatial-tracking` Permissions-Policy in embeds that weren't granted it.
|
|
256
|
+
if (this.useQuicklookExport) {
|
|
257
|
+
addUsdzExporterIfMissing();
|
|
258
|
+
}
|
|
259
|
+
else if (DeviceUtilities.isVisionOS()) {
|
|
260
|
+
NeedleXRSession.isARSupported().then((arSupported) => {
|
|
261
|
+
if (!arSupported) addUsdzExporterIfMissing();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
256
264
|
|
|
257
265
|
this.handleCreatingHTML();
|
|
258
266
|
this.handleOfferSession();
|