@needle-tools/engine 5.1.6 → 5.1.7

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 (62) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-FRzWLdVf.umd.cjs → needle-engine.bundle-1QN--joB.umd.cjs} +140 -140
  4. package/dist/{needle-engine.bundle-BcyvM7a6.js → needle-engine.bundle-DT6F1AXh.js} +4839 -4791
  5. package/dist/{needle-engine.bundle-CadxfFMM.min.js → needle-engine.bundle-DlX4Rcl_.min.js} +140 -140
  6. package/dist/needle-engine.d.ts +33 -15
  7. package/dist/needle-engine.js +28 -28
  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_license.d.ts +7 -7
  15. package/lib/engine/engine_license.js +71 -71
  16. package/lib/engine/engine_license.js.map +1 -1
  17. package/lib/engine/engine_networking_blob.js +3 -3
  18. package/lib/engine/engine_networking_blob.js.map +1 -1
  19. package/lib/engine/engine_utils.js +6 -0
  20. package/lib/engine/engine_utils.js.map +1 -1
  21. package/lib/engine/engine_utils_qrcode.js +2 -2
  22. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  23. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  24. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  25. package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
  26. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  27. package/lib/engine/webcomponents/needle-engine.js +2 -2
  28. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  29. package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
  30. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  31. package/lib/engine/xr/NeedleXRSession.js +20 -2
  32. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  33. package/lib/engine/xr/TempXRContext.js +2 -2
  34. package/lib/engine/xr/TempXRContext.js.map +1 -1
  35. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  36. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  37. package/lib/engine-components/webxr/WebARSessionRoot.d.ts +24 -6
  38. package/lib/engine-components/webxr/WebARSessionRoot.js +130 -39
  39. package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
  40. package/lib/engine-components/webxr/WebXR.d.ts +3 -3
  41. package/lib/engine-components/webxr/WebXR.js +4 -4
  42. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  43. package/package.json +1 -1
  44. package/plugins/common/license.js +23 -5
  45. package/plugins/vite/dependencies.js +34 -0
  46. package/plugins/vite/license.js +4 -4
  47. package/plugins/vite/needle-app.js +13 -0
  48. package/src/engine/api.ts +1 -1
  49. package/src/engine/engine_init.ts +2 -2
  50. package/src/engine/engine_license.ts +68 -68
  51. package/src/engine/engine_networking_blob.ts +3 -3
  52. package/src/engine/engine_utils.ts +4 -0
  53. package/src/engine/engine_utils_qrcode.ts +2 -2
  54. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  55. package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
  56. package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
  57. package/src/engine/webcomponents/needle-engine.ts +2 -2
  58. package/src/engine/xr/NeedleXRSession.ts +19 -2
  59. package/src/engine/xr/TempXRContext.ts +2 -2
  60. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  61. package/src/engine-components/webxr/WebARSessionRoot.ts +124 -38
  62. package/src/engine-components/webxr/WebXR.ts +4 -4
@@ -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,