@needle-tools/engine 5.1.7 → 5.1.8-next.1784885350.large-bamboo.746608e
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 +3 -0
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-DlX4Rcl_.min.js → needle-engine.bundle-Cw8tbJuS.min.js} +133 -133
- package/dist/{needle-engine.bundle-DT6F1AXh.js → needle-engine.bundle-bOImzZkB.js} +3895 -3880
- package/dist/{needle-engine.bundle-1QN--joB.umd.cjs → needle-engine.bundle-uEyfssJk.umd.cjs} +135 -135
- package/dist/needle-engine.d.ts +49 -18
- package/dist/needle-engine.js +334 -334
- 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 +71 -71
- 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_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.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 +2 -2
- package/lib/engine/xr/NeedleXRSession.js +9 -1
- 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/Component.d.ts +2 -1
- package/lib/engine-components/Component.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/TeleportTarget.d.ts +21 -8
- package/lib/engine-components/webxr/TeleportTarget.js +21 -8
- package/lib/engine-components/webxr/TeleportTarget.js.map +1 -1
- package/lib/engine-components/webxr/WebXRImageTracking.d.ts +1 -1
- package/lib/engine-components/webxr/WebXRImageTracking.js +19 -48
- package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
- package/lib/engine-components/webxr/WebXRImageTracking.utils.d.ts +35 -0
- package/lib/engine-components/webxr/WebXRImageTracking.utils.js +96 -0
- package/lib/engine-components/webxr/WebXRImageTracking.utils.js.map +1 -0
- package/lib/engine-components/webxr/controllers/XRControllerMovement.d.ts +18 -1
- package/lib/engine-components/webxr/controllers/XRControllerMovement.js +18 -1
- package/lib/engine-components/webxr/controllers/XRControllerMovement.js.map +1 -1
- package/package.json +1 -1
- 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 +68 -68
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_utils_qrcode.ts +2 -2
- 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 +11 -3
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/Component.ts +2 -1
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/webxr/TeleportTarget.ts +23 -10
- package/src/engine-components/webxr/WebXRImageTracking.ts +19 -51
- package/src/engine-components/webxr/WebXRImageTracking.utils.ts +105 -0
- package/src/engine-components/webxr/controllers/XRControllerMovement.ts +20 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { needleLogoOnlySVG } from "../assets/index.js"
|
|
2
2
|
import { isDevEnvironment, showBalloonWarning } from "../debug/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { _$BbU, _WXkIc, __UCuVxqBu } from "../engine_license.js";
|
|
4
4
|
import { Mathf } from "../engine_math.js";
|
|
5
5
|
import { LoadingProgressArgs } from "../engine_setup.js";
|
|
6
6
|
import { getParam } from "../engine_utils.js";
|
|
@@ -205,7 +205,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
|
|
208
|
-
const hasLicense =
|
|
208
|
+
const hasLicense = _WXkIc();
|
|
209
209
|
if (!existing) {
|
|
210
210
|
this._loadingElement.style.position = "absolute";
|
|
211
211
|
this._loadingElement.style.width = "100%";
|
|
@@ -373,7 +373,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
|
|
|
373
373
|
|
|
374
374
|
// private async handleRuntimeLicense(loadingElement: HTMLElement) {
|
|
375
375
|
// // First check if we have a commercial license
|
|
376
|
-
// let commercialLicense =
|
|
376
|
+
// let commercialLicense = _$BbU();
|
|
377
377
|
// // if it's the case then we don't need to perform a runtime check
|
|
378
378
|
// if (commercialLicense) return;
|
|
379
379
|
|
|
@@ -393,10 +393,10 @@ export class EngineLoadingView implements ILoadingViewHandler {
|
|
|
393
393
|
// loadingElement.appendChild(nonCommercialContainer);
|
|
394
394
|
|
|
395
395
|
// // Use the runtime license check
|
|
396
|
-
// if (!isDevEnvironment() &&
|
|
396
|
+
// if (!isDevEnvironment() && __UCuVxqBu) {
|
|
397
397
|
// if (debugLicense) console.log("Waiting for runtime license check");
|
|
398
|
-
// await
|
|
399
|
-
// commercialLicense =
|
|
398
|
+
// await __UCuVxqBu;
|
|
399
|
+
// commercialLicense = _$BbU();
|
|
400
400
|
// }
|
|
401
401
|
// if (commercialLicense) return;
|
|
402
402
|
// nonCommercialContainer.style.transition = "opacity .5s ease-in-out";
|
|
@@ -3,7 +3,7 @@ import type { Texture } from "three";
|
|
|
3
3
|
import { isDevEnvironment, showBalloonWarning } from "../debug/index.js";
|
|
4
4
|
import { PUBLIC_KEY, VERSION } from "../engine_constants.js";
|
|
5
5
|
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
|
|
6
|
-
import {
|
|
6
|
+
import { _$BbU, Telemetry } from "../engine_license.js";
|
|
7
7
|
import { onStart } from "../engine_lifecycle_api.js";
|
|
8
8
|
import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "../engine_loaders.gltf.js";
|
|
9
9
|
import { Context, ContextCreateArgs } from "../engine_setup.js";
|
|
@@ -579,7 +579,7 @@ export class NeedleEngineWebComponent extends HTMLElementBase implements INeedle
|
|
|
579
579
|
|
|
580
580
|
|
|
581
581
|
// Loading start events
|
|
582
|
-
const allowOverridingDefaultLoading =
|
|
582
|
+
const allowOverridingDefaultLoading = _$BbU();
|
|
583
583
|
// default loading can be overriden by calling preventDefault in the onload start event
|
|
584
584
|
this.ensureLoadStartIsRegistered();
|
|
585
585
|
let useDefaultLoading = this.dispatchEvent(new CustomEvent("loadstart", {
|
|
@@ -72,8 +72,8 @@ let debugFPSFramesSinceLastUpdate = 0;
|
|
|
72
72
|
export interface INeedleXRSessionEventReceiver extends Pick<IComponent, "destroyed"> {
|
|
73
73
|
get activeAndEnabled(): boolean;
|
|
74
74
|
supportsXR?(mode: XRSessionMode): boolean;
|
|
75
|
-
/** Called before requesting a XR session */
|
|
76
|
-
onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void
|
|
75
|
+
/** Called before requesting a XR session. May return a promise that is awaited before requesting. */
|
|
76
|
+
onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void | Promise<void>;
|
|
77
77
|
onEnterXR?(args: NeedleXREventArgs): void;
|
|
78
78
|
onUpdateXR?(args: NeedleXREventArgs): void;
|
|
79
79
|
onLeaveXR?(args: NeedleXREventArgs): void;
|
|
@@ -757,7 +757,15 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
757
757
|
console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;");
|
|
758
758
|
for (const script of scripts) {
|
|
759
759
|
if (script.onBeforeXR && script.activeAndEnabled && !script.destroyed) {
|
|
760
|
-
|
|
760
|
+
try {
|
|
761
|
+
// Await: onBeforeXR may be async (e.g. WebXRImageTracking awaits marker bitmap loads
|
|
762
|
+
// so late-decoding markers aren't dropped). A failing hook must not abort the session
|
|
763
|
+
// request for every other component.
|
|
764
|
+
await script.onBeforeXR(mode, init);
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
console.error("[NeedleXRSession] onBeforeXR hook failed:", err);
|
|
768
|
+
}
|
|
761
769
|
}
|
|
762
770
|
}
|
|
763
771
|
this.invokeSessionRequestStart(mode, init, context ?? Context.Current);
|
|
@@ -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 { _$BbU } 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 (_$BbU()) {
|
|
219
219
|
const htmlComponent = document.querySelector("needle-engine");
|
|
220
220
|
if (htmlComponent) {
|
|
221
221
|
const licenseLogo = htmlComponent.getAttribute("logo-src");
|
|
@@ -930,8 +930,9 @@ export abstract class Component implements IComponent, EventTarget,
|
|
|
930
930
|
* Use to modify session initialization parameters
|
|
931
931
|
* @param mode The XR session mode being requested
|
|
932
932
|
* @param args The session initialization parameters that can be modified
|
|
933
|
+
* @returns void, or a promise that is awaited before the session is requested (e.g. to load assets)
|
|
933
934
|
*/
|
|
934
|
-
onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void
|
|
935
|
+
onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void | Promise<void>;
|
|
935
936
|
|
|
936
937
|
/**
|
|
937
938
|
* Called when this component joins an XR session or becomes active in a running session
|
|
@@ -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 { _WXkIc } 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 (!_WXkIc()) {
|
|
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 (!_WXkIc()) {
|
|
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 (!_WXkIc()) {
|
|
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";
|
|
@@ -2,26 +2,39 @@ import { Behaviour } from "../Component.js";
|
|
|
2
2
|
import type { XRControllerMovement } from "./controllers/XRControllerMovement.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Marks a GameObject as a valid teleportation target for VR locomotion.
|
|
6
|
-
* Add this component to objects or surfaces where users should be able to teleport.
|
|
5
|
+
* Marks a GameObject as a valid teleportation target for VR locomotion.
|
|
6
|
+
* Add this component to objects or surfaces where users should be able to teleport.
|
|
7
7
|
*
|
|
8
|
-
* **
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* **How targeting works (important):**
|
|
9
|
+
* The teleport ray uses a **visual raycast against rendered meshes** — NOT physics colliders.
|
|
10
|
+
* When {@link XRControllerMovement.useTeleportTarget} is enabled, the controller ray is cast
|
|
11
|
+
* against the rendered scene and the hit object (or any of its parents) is checked for a
|
|
12
|
+
* `TeleportTarget` component. You can only teleport onto an object if that object, or one of
|
|
13
|
+
* its ancestors, has this component.
|
|
14
|
+
*
|
|
15
|
+
* **Requirements for a target to be hittable:**
|
|
16
|
+
* - It MUST have a visible, rendered mesh (a `MeshRenderer` / geometry). The ray hits the
|
|
17
|
+
* rendered geometry directly.
|
|
18
|
+
* - **A collider is NOT required.** Teleport does not use physics colliders — adding a
|
|
19
|
+
* `BoxCollider` (or any collider) has no effect on whether the target can be teleported to.
|
|
20
|
+
* - It must NOT be on the IgnoreRaycast layer (three.js layer 2) and must not have
|
|
21
|
+
* `raycastAllowed` / `visible` set to false, otherwise the ray skips it.
|
|
22
|
+
* - The `TeleportTarget` component may be on the hit mesh itself or on any parent
|
|
23
|
+
* (resolved via `getComponentInParent`).
|
|
12
24
|
*
|
|
13
25
|
* **Setup:**
|
|
14
|
-
* 1. Add this component to GameObjects that should be teleport destinations
|
|
15
|
-
* 2.
|
|
26
|
+
* 1. Add this component to GameObjects (with a visible mesh) that should be teleport destinations
|
|
27
|
+
* 2. Enable "Use Teleport Target" on the {@link XRControllerMovement} component so teleporting
|
|
28
|
+
* is restricted to objects carrying this component
|
|
16
29
|
* 3. Test teleportation in VR mode
|
|
17
30
|
*
|
|
18
31
|
* @example
|
|
19
32
|
* ```ts
|
|
20
|
-
* // Make a platform teleportable
|
|
33
|
+
* // Make a platform teleportable (the platform already has a visible mesh)
|
|
21
34
|
* const platform = myPlatform.addComponent(TeleportTarget);
|
|
22
35
|
* ```
|
|
23
36
|
*
|
|
24
|
-
* @summary Marker component for valid VR teleportation destinations
|
|
37
|
+
* @summary Marker component for valid VR teleportation destinations (mesh hit-test, no collider needed)
|
|
25
38
|
* @category XR
|
|
26
39
|
* @group Components
|
|
27
40
|
* @see {@link XRControllerMovement} for VR locomotion and teleport configuration
|
|
@@ -14,6 +14,7 @@ import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.j
|
|
|
14
14
|
import { Behaviour, GameObject } from "../Component.js";
|
|
15
15
|
import { EventList } from "../EventList.js";
|
|
16
16
|
import { Renderer } from "../Renderer.js";
|
|
17
|
+
import { _imageElements, collectTrackedImages, loadImage } from "./WebXRImageTracking.utils.js";
|
|
17
18
|
|
|
18
19
|
// https://github.com/immersive-web/marker-tracking/blob/main/explainer.md
|
|
19
20
|
|
|
@@ -709,26 +710,25 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
709
710
|
|
|
710
711
|
|
|
711
712
|
/** @internal */
|
|
712
|
-
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): void {
|
|
713
|
-
|
|
714
|
-
if (this.trackedImages) {
|
|
715
|
-
args.optionalFeatures = args.optionalFeatures || [];
|
|
716
|
-
if (!args.optionalFeatures.includes("image-tracking"))
|
|
717
|
-
args.optionalFeatures.push("image-tracking");
|
|
713
|
+
async onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): Promise<void> {
|
|
714
|
+
if (!this.trackedImages) return;
|
|
718
715
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
716
|
+
args.optionalFeatures = args.optionalFeatures || [];
|
|
717
|
+
if (!args.optionalFeatures.includes("image-tracking"))
|
|
718
|
+
args.optionalFeatures.push("image-tracking");
|
|
719
|
+
|
|
720
|
+
// Resolve the marker bitmaps *before* the session is requested.
|
|
721
|
+
// onBeforeXR runs the moment "Enter AR" is tapped. Marker images load asynchronously
|
|
722
|
+
// (network fetch + createImageBitmap), so without awaiting here a marker that hasn't finished
|
|
723
|
+
// decoding yet would be silently dropped from `trackedImages` and never registered with the
|
|
724
|
+
// native/WebXR session - it would then only start working after exiting and re-entering AR,
|
|
725
|
+
// once the bitmap has been cached. Awaiting the (idempotent) loads closes that race.
|
|
726
|
+
// A marker that can not be loaded resolves to null and is skipped individually, so a single
|
|
727
|
+
// unavailable image never rejects the whole XR session request. See forum topic #2859.
|
|
728
|
+
this.trackedImageIndexMap.clear();
|
|
729
|
+
const indexMap = await collectTrackedImages(this.trackedImages, args);
|
|
730
|
+
for (const [index, model] of indexMap) {
|
|
731
|
+
this.trackedImageIndexMap.set(index, model);
|
|
732
732
|
}
|
|
733
733
|
}
|
|
734
734
|
|
|
@@ -942,36 +942,4 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
942
942
|
}
|
|
943
943
|
}
|
|
944
944
|
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const _imageElements: Map<string, ImageBitmap | null> = new Map();
|
|
951
|
-
const _imageLoadingPromises: Map<string, Promise<boolean>> = new Map();
|
|
952
|
-
|
|
953
|
-
async function loadImage(url: string) {
|
|
954
|
-
if (_imageElements.has(url)) {
|
|
955
|
-
if (_imageLoadingPromises.has(url)) return _imageLoadingPromises.get(url);
|
|
956
|
-
return Promise.resolve(true);
|
|
957
|
-
}
|
|
958
|
-
const promise = new Promise<boolean>(res => {
|
|
959
|
-
_imageElements.set(url, null);
|
|
960
|
-
if(isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Start loading image for tracking: ${url}`);
|
|
961
|
-
const imageElement = document.createElement("img") as HTMLImageElement;
|
|
962
|
-
imageElement.src = url;
|
|
963
|
-
imageElement.addEventListener("load", async () => {
|
|
964
|
-
if(isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Loaded image for tracking: ${url}`);
|
|
965
|
-
const img = await createImageBitmap(imageElement);
|
|
966
|
-
_imageElements.set(url, img);
|
|
967
|
-
res(true);
|
|
968
|
-
});
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
_imageLoadingPromises.set(url, promise);
|
|
972
|
-
promise.finally(() => {
|
|
973
|
-
_imageLoadingPromises.delete(url);
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
return promise;
|
|
977
945
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
|
2
|
+
import { getParam } from "../../engine/engine_utils.js";
|
|
3
|
+
|
|
4
|
+
import type { WebXRImageTrackingModel } from "./WebXRImageTracking.js";
|
|
5
|
+
|
|
6
|
+
// Kept in a dedicated module (with only lightweight imports) so the marker-loading logic can be unit
|
|
7
|
+
// tested without pulling in the heavy WebXRImageTracking / USDZ exporter import graph.
|
|
8
|
+
|
|
9
|
+
const debug = getParam("debugimagetracking");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cache of decoded marker bitmaps, keyed by image url.
|
|
13
|
+
* Also read by the USDZ exporter (see {@link WebXRImageTracking}) to embed the marker into the .usdz.
|
|
14
|
+
*/
|
|
15
|
+
export const _imageElements: Map<string, ImageBitmap | null> = new Map();
|
|
16
|
+
const _imageLoadingPromises: Map<string, Promise<ImageBitmap | null>> = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve marker bitmaps for the given tracked-image models and append them to `args.trackedImages`
|
|
20
|
+
* (the list handed to `navigator.xr.requestSession`).
|
|
21
|
+
*
|
|
22
|
+
* Every valid marker is awaited so that images which are still decoding when the XR session is
|
|
23
|
+
* requested get included instead of being silently dropped (which previously caused markers to only
|
|
24
|
+
* work after re-entering AR). Loading happens in parallel and never rejects: a marker that can not be
|
|
25
|
+
* loaded is skipped individually so a single unavailable image can not fail the whole session request.
|
|
26
|
+
*
|
|
27
|
+
* @param models The configured tracked-image models
|
|
28
|
+
* @param args The session init object; `trackedImages` is created if missing and appended to
|
|
29
|
+
* @param load Injectable loader (defaults to {@link loadImage}); resolves the decoded bitmap or null
|
|
30
|
+
* @returns Map of `trackedImages` index -> model for the appended images (used to resolve results)
|
|
31
|
+
*/
|
|
32
|
+
export async function collectTrackedImages(
|
|
33
|
+
models: readonly WebXRImageTrackingModel[] | undefined | null,
|
|
34
|
+
args: { trackedImages?: Array<{ image: ImageBitmap, widthInMeters: number }> },
|
|
35
|
+
load: (url: string) => Promise<ImageBitmap | null> = loadImage,
|
|
36
|
+
): Promise<Map<number, WebXRImageTrackingModel>> {
|
|
37
|
+
const indexMap = new Map<number, WebXRImageTrackingModel>();
|
|
38
|
+
if (!args.trackedImages) args.trackedImages = [];
|
|
39
|
+
if (!models) return indexMap;
|
|
40
|
+
|
|
41
|
+
const valid = models.filter(m => !!m.image?.length && m.widthInMeters > 0);
|
|
42
|
+
// Load in parallel and wait for all of them. Guard every load with a catch so that a rejecting
|
|
43
|
+
// loader is treated as "unavailable" (null) rather than aborting the whole session request.
|
|
44
|
+
const bitmaps = await Promise.all(valid.map(m => load(m.image!).catch(() => null)));
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < valid.length; i++) {
|
|
47
|
+
const model = valid[i];
|
|
48
|
+
const bitmap = bitmaps[i];
|
|
49
|
+
if (bitmap) {
|
|
50
|
+
indexMap.set(args.trackedImages.length, model);
|
|
51
|
+
args.trackedImages.push({ image: bitmap, widthInMeters: model.widthInMeters });
|
|
52
|
+
}
|
|
53
|
+
else if (isDevEnvironment() || debug) {
|
|
54
|
+
console.warn(`[WebXRImageTracking] Marker image could not be loaded, skipping: ${model.image}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return indexMap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load and decode a marker image into an {@link ImageBitmap}, caching it in {@link _imageElements}.
|
|
62
|
+
* Idempotent: concurrent or repeated calls for the same URL share a single in-flight load.
|
|
63
|
+
*
|
|
64
|
+
* Resolves to `null` and never rejects when the image can not be fetched or decoded, so callers can
|
|
65
|
+
* skip an unavailable marker without failing the surrounding XR session request. A failed load clears
|
|
66
|
+
* its cache entry so a later attempt (e.g. re-entering AR) can retry.
|
|
67
|
+
*/
|
|
68
|
+
export async function loadImage(url: string): Promise<ImageBitmap | null> {
|
|
69
|
+
const cached = _imageElements.get(url);
|
|
70
|
+
if (cached) return cached;
|
|
71
|
+
const pending = _imageLoadingPromises.get(url);
|
|
72
|
+
if (pending) return pending;
|
|
73
|
+
|
|
74
|
+
const promise = new Promise<ImageBitmap | null>(res => {
|
|
75
|
+
_imageElements.set(url, null); // placeholder to dedupe concurrent loads
|
|
76
|
+
if (isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Start loading image for tracking: ${url}`);
|
|
77
|
+
const imageElement = document.createElement("img") as HTMLImageElement;
|
|
78
|
+
imageElement.addEventListener("load", async () => {
|
|
79
|
+
try {
|
|
80
|
+
const img = await createImageBitmap(imageElement);
|
|
81
|
+
_imageElements.set(url, img);
|
|
82
|
+
if (isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Loaded image for tracking: ${url}`);
|
|
83
|
+
res(img);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(`[WebXRImageTracking] Failed to decode marker image for tracking: ${url}`, err);
|
|
87
|
+
_imageElements.delete(url); // allow a retry later (e.g. re-entering AR)
|
|
88
|
+
res(null);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
imageElement.addEventListener("error", (evt) => {
|
|
92
|
+
console.error(`[WebXRImageTracking] Failed to load marker image for tracking: ${url}`, evt);
|
|
93
|
+
_imageElements.delete(url); // allow a retry later (e.g. re-entering AR)
|
|
94
|
+
res(null);
|
|
95
|
+
});
|
|
96
|
+
imageElement.src = url;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
_imageLoadingPromises.set(url, promise);
|
|
100
|
+
promise.finally(() => {
|
|
101
|
+
_imageLoadingPromises.delete(url);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return promise;
|
|
105
|
+
}
|
|
@@ -28,8 +28,17 @@ declare type HitPointObject = Object3D & { material: Material & { opacity: numbe
|
|
|
28
28
|
*
|
|
29
29
|
* Additionally it supports teleporting using the right controller's thumbstick or by pinching the index finger tip in front of the hand (if hand tracking is enabled).
|
|
30
30
|
* It also visualizes controller rays and hit points in the scene.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
*
|
|
32
|
+
* The component operates on the active XR rig and controllers from the {@link NeedleXRSession} (`args.xr.rig`,
|
|
33
|
+
* `args.xr.leftController` / `rightController`) and receives `onUpdateXR` regardless of where it sits in the hierarchy.
|
|
34
|
+
* It does NOT need to be on the same GameObject as {@link WebXR}, and does not read {@link WebXR} from its parents.
|
|
35
|
+
*
|
|
36
|
+
* **Duplicate-handler gotcha:** {@link WebXR.setDefaultMovementEnabled} (called when `WebXR.useDefaultControls` is enabled,
|
|
37
|
+
* the default) adds an `XRControllerMovement` to the WebXR GameObject only if that object has none. If you place your own
|
|
38
|
+
* `XRControllerMovement` on a different object, WebXR still adds a second, default one at runtime — two handlers then run
|
|
39
|
+
* simultaneously (doubled movement, and your teleport settings such as {@link XRControllerMovement.useTeleportTarget} are
|
|
40
|
+
* bypassed by the default handler). Put your component on the WebXR GameObject, or disable `WebXR.useDefaultControls`.
|
|
41
|
+
*
|
|
33
42
|
* @summary Move the XR rig using controller input
|
|
34
43
|
* @category XR
|
|
35
44
|
* @group Components
|
|
@@ -61,7 +70,15 @@ export class XRControllerMovement extends Behaviour implements XRMovementBehavio
|
|
|
61
70
|
@serializable()
|
|
62
71
|
usePinchToTeleport: boolean = true;
|
|
63
72
|
|
|
64
|
-
/** Enable to only allow teleporting on objects with a
|
|
73
|
+
/** Enable to only allow teleporting on objects with a {@link TeleportTarget} component.
|
|
74
|
+
*
|
|
75
|
+
* The teleport ray is a **visual raycast against rendered meshes** (not physics colliders):
|
|
76
|
+
* it hits the rendered geometry and then looks for a {@link TeleportTarget} on the hit object
|
|
77
|
+
* or any of its parents. A collider is NOT required on the target — only a visible mesh that
|
|
78
|
+
* is not on the IgnoreRaycast layer.
|
|
79
|
+
*
|
|
80
|
+
* When enabled, teleporting is rejected if the ray does not hit a `TeleportTarget` (the
|
|
81
|
+
* ground-plane fallback used in free teleport mode is also disabled).
|
|
65
82
|
* @default false
|
|
66
83
|
*/
|
|
67
84
|
@serializable()
|