@needle-tools/engine 5.1.6 → 5.1.8

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-BcyvM7a6.js → needle-engine.bundle-CaD5rOJ0.js} +4839 -4791
  4. package/dist/{needle-engine.bundle-FRzWLdVf.umd.cjs → needle-engine.bundle-D8gP8Nd0.umd.cjs} +140 -140
  5. package/dist/{needle-engine.bundle-CadxfFMM.min.js → needle-engine.bundle-X7Y4A-EO.min.js} +140 -140
  6. package/dist/needle-engine.d.ts +72 -24
  7. package/dist/needle-engine.js +266 -266
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/lib/engine/api.d.ts +1 -1
  11. package/lib/engine/api.js +1 -1
  12. package/lib/engine/api.js.map +1 -1
  13. package/lib/engine/engine_init.js +2 -2
  14. package/lib/engine/engine_init.js.map +1 -1
  15. package/lib/engine/engine_license.d.ts +7 -7
  16. package/lib/engine/engine_license.js +71 -71
  17. package/lib/engine/engine_license.js.map +1 -1
  18. package/lib/engine/engine_networking_blob.js +3 -3
  19. package/lib/engine/engine_networking_blob.js.map +1 -1
  20. package/lib/engine/engine_utils.js +6 -0
  21. package/lib/engine/engine_utils.js.map +1 -1
  22. package/lib/engine/engine_utils_qrcode.js +2 -2
  23. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  24. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  25. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  26. package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
  27. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  28. package/lib/engine/webcomponents/needle-engine.js +2 -2
  29. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  30. package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
  31. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  32. package/lib/engine/xr/NeedleXRSession.js +20 -2
  33. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  34. package/lib/engine/xr/TempXRContext.js +2 -2
  35. package/lib/engine/xr/TempXRContext.js.map +1 -1
  36. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  37. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  38. package/lib/engine-components/webxr/TeleportTarget.d.ts +21 -8
  39. package/lib/engine-components/webxr/TeleportTarget.js +21 -8
  40. package/lib/engine-components/webxr/TeleportTarget.js.map +1 -1
  41. package/lib/engine-components/webxr/WebARSessionRoot.d.ts +24 -6
  42. package/lib/engine-components/webxr/WebARSessionRoot.js +130 -39
  43. package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
  44. package/lib/engine-components/webxr/WebXR.d.ts +3 -3
  45. package/lib/engine-components/webxr/WebXR.js +4 -4
  46. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  47. package/lib/engine-components/webxr/controllers/XRControllerMovement.d.ts +18 -1
  48. package/lib/engine-components/webxr/controllers/XRControllerMovement.js +18 -1
  49. package/lib/engine-components/webxr/controllers/XRControllerMovement.js.map +1 -1
  50. package/package.json +1 -1
  51. package/plugins/common/license.js +23 -5
  52. package/plugins/vite/dependencies.js +34 -0
  53. package/plugins/vite/license.js +4 -4
  54. package/plugins/vite/needle-app.js +13 -0
  55. package/src/engine/api.ts +1 -1
  56. package/src/engine/engine_init.ts +2 -2
  57. package/src/engine/engine_license.ts +68 -68
  58. package/src/engine/engine_networking_blob.ts +3 -3
  59. package/src/engine/engine_utils.ts +4 -0
  60. package/src/engine/engine_utils_qrcode.ts +2 -2
  61. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  62. package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
  63. package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
  64. package/src/engine/webcomponents/needle-engine.ts +2 -2
  65. package/src/engine/xr/NeedleXRSession.ts +19 -2
  66. package/src/engine/xr/TempXRContext.ts +2 -2
  67. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  68. package/src/engine-components/webxr/TeleportTarget.ts +23 -10
  69. package/src/engine-components/webxr/WebARSessionRoot.ts +124 -38
  70. package/src/engine-components/webxr/WebXR.ts +4 -4
  71. package/src/engine-components/webxr/controllers/XRControllerMovement.ts +20 -3
@@ -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
- * **Usage:**
9
- * - Add to floor surfaces, platforms, or designated teleport spots
10
- * - Works with {@link XRControllerMovement} component's teleport system
11
- * - Can be used to restrict teleportation to specific areas
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. Configure {@link XRControllerMovement} to use teleport targets (optional)
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
@@ -18,6 +18,22 @@ const debug = getParam("debugwebxr");
18
18
 
19
19
  const invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
20
20
 
21
+ /** a XRRigidTransform pose stored as plain values — XR hit test results and frames are only
22
+ * valid within the animation frame that produced them, so poses that need to survive until a
23
+ * later frame (or an input event) must be snapshotted */
24
+ type XRAnchorRefPose = { x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number };
25
+
26
+ const unitScale = new Vector3(1, 1, 1);
27
+ const tempAnchorPose = new Matrix4();
28
+ const tempAnchorDelta = new Matrix4();
29
+
30
+ function matrixApproximatelyEquals(a: Matrix4, b: Matrix4, epsilon: number = 1e-6): boolean {
31
+ for (let i = 0; i < 16; i++) {
32
+ if (Math.abs(a.elements[i] - b.elements[i]) > epsilon) return false;
33
+ }
34
+ return true;
35
+ }
36
+
21
37
 
22
38
  /**
23
39
  * The WebARSessionRoot is the root object for a WebAR session and used to place the scene in AR.
@@ -101,11 +117,12 @@ export class WebARSessionRoot extends Behaviour {
101
117
  /** When enabled the scene center will be automatically calculated from the content in the scene */
102
118
  autoCenter: boolean = false;
103
119
 
104
- /** Experimental: When enabled we will create a XR anchor for the scene placement
105
- * and make sure the scene is at that anchored point during a XR session
106
- * @default false
120
+ /** When enabled a XR anchor is created at the scene placement position and the scene
121
+ * stays at that anchored point while tracking refines during the XR session.
122
+ * On platforms without WebXR anchor support this gracefully falls back to plain placement.
123
+ * @default true
107
124
  **/
108
- useXRAnchor: boolean = false;
125
+ useXRAnchor: boolean = true;
109
126
 
110
127
  /** true if we're currently placing the scene */
111
128
  private _isPlacing = true;
@@ -125,13 +142,18 @@ export class WebARSessionRoot extends Behaviour {
125
142
 
126
143
  /** the reticles used for placement */
127
144
  private readonly _reticle: IGameObject[] = [];
128
- /** needs to be in sync with the reticles */
129
- private readonly _hits: XRHitTestResult[] = [];
145
+ /** reference-space hit poses, in sync with the reticles — captured while placing when {@link useXRAnchor}
146
+ * is enabled, because hit test results can not be used outside the animation frame that produced them */
147
+ private readonly _hitRefPoses: (XRAnchorRefPose | undefined)[] = [];
130
148
 
131
149
  private _placementStartTime: number = -1;
132
150
  private _rigPlacementMatrix?: Matrix4;
133
151
  /** if useAnchor is enabled this is the anchor we have created on placing the scene using the placement hit */
134
152
  private _anchor: XRAnchor | null = null;
153
+ /** the placement pose (reference space) waiting for anchor creation on the next animation frame */
154
+ private _pendingAnchorPose: XRAnchorRefPose | null = null;
155
+ /** the last anchor pose (in rig space) that has been applied to the rig */
156
+ private readonly _anchorLastLocalPose = new Matrix4();
135
157
  /** user input is used for ar touch transform */
136
158
  private userInput?: WebXRSessionRootUserInput;
137
159
 
@@ -147,6 +169,8 @@ export class WebARSessionRoot extends Behaviour {
147
169
  if (debug) console.log("ENTER WEBXR: SessionRoot start...");
148
170
 
149
171
  this._anchor = null;
172
+ this._pendingAnchorPose = null;
173
+ this._hitRefPoses.length = 0;
150
174
  WebARSessionRoot._hasPlaced = false;
151
175
 
152
176
  // if (_args.xr.session.enabledFeatures?.includes("image-tracking")) {
@@ -205,8 +229,9 @@ export class WebARSessionRoot extends Behaviour {
205
229
  // TODO: WebARSessionRoot doesnt work when we enter passthrough and leave XR without having placed the session!!!
206
230
  this.context.input.removeEventListener("pointerup", this.onPlaceScene, { queue: InputEventQueue.Early });
207
231
  this.onRevertSceneChanges();
208
- // this._anchor?.delete();
232
+ // no explicit anchor.delete() — anchors are released by the platform when the session ends
209
233
  this._anchor = null;
234
+ this._pendingAnchorPose = null;
210
235
  WebARSessionRoot._hasPlaced = false;
211
236
  this._rigPlacementMatrix = undefined;
212
237
  }
@@ -252,20 +277,8 @@ export class WebARSessionRoot extends Behaviour {
252
277
 
253
278
  }
254
279
  else {
255
- // Update anchors, if any
256
- if (this._anchor && args.xr.referenceSpace) {
257
- const pose = args.xr.frame.getPose(this._anchor.anchorSpace, args.xr.referenceSpace);
258
- if (pose && this.context.time.frame % 20 === 0) {
259
- // apply the anchor pose to one of the reticles
260
- const converted = args.xr.convertSpace(pose.transform);
261
- const reticle = this._reticle[0];
262
- if (reticle) {
263
- reticle.position.copy(converted.position);
264
- reticle.quaternion.copy(converted.quaternion);
265
- this.onApplyPose(reticle);
266
- }
267
- }
268
- }
280
+ // create the anchor (deferred from placement) and keep the scene glued to it
281
+ if (this.useXRAnchor) this.updateXRAnchor(args.xr);
269
282
 
270
283
  // Scene has been placed
271
284
  if (this.arTouchTransform) {
@@ -286,9 +299,20 @@ export class WebARSessionRoot extends Behaviour {
286
299
  }
287
300
  }
288
301
 
289
- private updateReticleAndHits(_xr: NeedleXRSession, i: number, hit: NeedleXRHitTestResult, scale: number) {
290
- // save the hit test
291
- this._hits[i] = hit.hit;
302
+ private updateReticleAndHits(xr: NeedleXRSession, i: number, hit: NeedleXRHitTestResult, scale: number) {
303
+ // capture the reference-space hit pose while the XRFrame that produced the hit is still
304
+ // active — it can not be queried anymore during placement (which runs from an input event)
305
+ if (this.useXRAnchor) {
306
+ const referenceSpace = xr.referenceSpace;
307
+ const rawPose = referenceSpace ? hit.hit.getPose(referenceSpace) : undefined;
308
+ if (rawPose) {
309
+ let pose = this._hitRefPoses[i];
310
+ if (!pose) pose = this._hitRefPoses[i] = { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1 };
311
+ const { position, orientation } = rawPose.transform;
312
+ pose.x = position.x; pose.y = position.y; pose.z = position.z;
313
+ pose.qx = orientation.x; pose.qy = orientation.y; pose.qz = orientation.z; pose.qw = orientation.w;
314
+ }
315
+ }
292
316
  let reticle = this._reticle[i];
293
317
  if (!reticle) {
294
318
  if (this.customReticle) {
@@ -392,14 +416,14 @@ export class WebARSessionRoot extends Behaviour {
392
416
  return;
393
417
  }
394
418
 
395
- let hit = this._hits[0];
419
+ let hitIndex = 0;
396
420
 
397
421
  if (evt && evt.origin instanceof NeedleXRController) {
398
422
  // until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
399
423
  const controllerReticle = this._reticle[evt.origin.index];
400
424
  if (controllerReticle) {
401
425
  reticle = controllerReticle;
402
- hit = this._hits[evt.origin.index];
426
+ hitIndex = evt.origin.index;
403
427
  }
404
428
  }
405
429
 
@@ -423,7 +447,15 @@ export class WebARSessionRoot extends Behaviour {
423
447
  WebARSessionRoot._hasPlaced = true;
424
448
 
425
449
  if (this.useXRAnchor) {
426
- this.onCreateAnchor(NeedleXRSession.active!, hit);
450
+ // anchor creation is deferred to the next animation frame — see updateXRAnchor
451
+ const refPose = this._hitRefPoses[hitIndex];
452
+ if (refPose) {
453
+ this._pendingAnchorPose = { ...refPose };
454
+ }
455
+ else {
456
+ console.warn("[WebARSessionRoot] can not create a XR anchor: no hit pose was captured for the placement");
457
+ if (isDevEnvironment()) showBalloonWarning("Can not create XR anchor: no hit pose available");
458
+ }
427
459
  }
428
460
 
429
461
  if (this.context.xr) {
@@ -464,19 +496,73 @@ export class WebARSessionRoot extends Behaviour {
464
496
  }
465
497
  }
466
498
 
467
- private async onCreateAnchor(session: NeedleXRSession, hit: XRHitTestResult) {
468
- if (hit.createAnchor === undefined) {
469
- console.warn("Hit does not support creating an anchor", hit);
470
- if (isDevEnvironment()) showBalloonWarning("Hit does not support creating an anchor");
499
+ /**
500
+ * Deferred anchor creation and per-frame anchor tracking. Called from {@link onUpdateXR} after the scene has been placed.
501
+ *
502
+ * Creation can not happen during placement: `XRHitTestResult.createAnchor` only works while the animation
503
+ * frame that produced the hit is active, and placement runs from an input event where that frame is already
504
+ * over (`XRFrame.createAnchor` equally requires an active animation frame). So {@link onPlaceScene} only
505
+ * records the reference-space pose and the anchor is created here, on the next animation frame.
506
+ *
507
+ * Once the anchor exists, its pose *changes* (delta in rig space) are applied to the XR rig: the world pose
508
+ * of the anchored point stays constant while tracking refines, without discarding other rig modifications
509
+ * ({@link arTouchTransform} user adjustments, {@link arScale}).
510
+ */
511
+ private updateXRAnchor(xr: NeedleXRSession) {
512
+ const referenceSpace = xr.referenceSpace;
513
+ if (!referenceSpace) return;
514
+ const frame = xr.frame;
515
+
516
+ if (this._pendingAnchorPose) {
517
+ const pending = this._pendingAnchorPose;
518
+ this._pendingAnchorPose = null;
519
+ if (typeof frame.createAnchor !== "function" || xr.session.enabledFeatures?.includes("anchors") === false) {
520
+ // dev-only: useXRAnchor is enabled by default, so platforms without anchor
521
+ // support (e.g. iOS) would otherwise warn in every production session
522
+ if (debug || isDevEnvironment()) {
523
+ console.warn("[WebARSessionRoot] useXRAnchor is enabled but WebXR anchors are not supported by this session");
524
+ showBalloonWarning("WebXR anchors are not supported by this session");
525
+ }
526
+ return;
527
+ }
528
+ const transform = new XRRigidTransform({ x: pending.x, y: pending.y, z: pending.z }, { x: pending.qx, y: pending.qy, z: pending.qz, w: pending.qw });
529
+ // the anchor is created exactly at this pose — remember it (in rig space) as the baseline for deltas
530
+ const initialPose = xr.convertSpace(transform);
531
+ this._anchorLastLocalPose.compose(initialPose.position, initialPose.quaternion, unitScale);
532
+ frame.createAnchor(transform, referenceSpace).then(anchor => {
533
+ if (xr.running && !this._isPlacing) {
534
+ this._anchor = anchor;
535
+ if (debug) console.log("[WebARSessionRoot] created XR anchor", anchor);
536
+ }
537
+ else anchor.delete();
538
+ }).catch((err: unknown) => {
539
+ console.warn("[WebARSessionRoot] failed to create XR anchor", err);
540
+ if (isDevEnvironment()) showBalloonWarning("Failed to create XR anchor");
541
+ });
471
542
  return;
472
543
  }
473
- else {
474
- // @ts-ignore
475
- const anchor = await hit.createAnchor(session.viewerPose!.transform);
476
- // make sure the session is still active
477
- if (session.running && anchor) {
478
- this._anchor = anchor;
479
- }
544
+
545
+ if (!this._anchor) return;
546
+ const rig = xr.rig?.gameObject;
547
+ if (!rig) return;
548
+ const pose = frame.getPose(this._anchor.anchorSpace, referenceSpace);
549
+ // the anchor may temporarily not be tracked — keep the last applied correction then
550
+ if (!pose) return;
551
+
552
+ const current = xr.convertSpace(pose.transform);
553
+ tempAnchorPose.compose(current.position, current.quaternion, unitScale);
554
+ if (matrixApproximatelyEquals(tempAnchorPose, this._anchorLastLocalPose)) return;
555
+
556
+ // rig_new = rig_old · lastPose · currentPose⁻¹ — keeps the world pose of the anchored
557
+ // point (rigWorld · anchorRigSpacePose) constant across tracking refinements
558
+ tempAnchorDelta.copy(tempAnchorPose).invert().premultiply(this._anchorLastLocalPose);
559
+ this._anchorLastLocalPose.copy(tempAnchorPose);
560
+ rig.updateMatrix();
561
+ rig.matrix.multiply(tempAnchorDelta);
562
+ rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
563
+ if (debug) {
564
+ const dx = tempAnchorDelta.elements[12], dy = tempAnchorDelta.elements[13], dz = tempAnchorDelta.elements[14];
565
+ console.log(`[WebARSessionRoot] applied XR anchor correction: ${(Math.hypot(dx, dy, dz) * 1000).toFixed(2)}mm`);
480
566
  }
481
567
  }
482
568
 
@@ -152,12 +152,12 @@ export class WebXR extends Behaviour {
152
152
  arScale: number = 1;
153
153
 
154
154
  /**
155
- * When enabled, an XRAnchor will be created for the AR scene and its position will be regularly updated to match the anchor.
156
- * This can help with spatial persistence in AR experiences.
157
- * @experimental
155
+ * When enabled, an XRAnchor is created at the AR placement position and the scene stays glued to it while tracking refines.
156
+ * On platforms without WebXR anchor support this gracefully falls back to plain placement.
157
+ * @default true
158
158
  */
159
159
  @serializable()
160
- useXRAnchor: boolean = false;
160
+ useXRAnchor: boolean = true;
161
161
 
162
162
  /**
163
163
  * When enabled, the scene will be automatically placed as soon as a suitable surface is detected in AR,
@@ -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 TeleportTarget component (see {@link TeleportTarget})
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()