Needle Engine

Changes between version 4.1.1 and 4.2.0
Files changed (7) hide show
  1. src/engine-components/DragControls.ts +13 -3
  2. src/engine/engine_utils.ts +7 -0
  3. src/engine/xr/NeedleXRController.ts +0 -14
  4. src/engine/xr/NeedleXRSession.ts +10 -2
  5. src/engine-components/webxr/WebXRPlaneTracking.ts +2 -2
  6. src/engine-components/webxr/controllers/XRControllerFollow.ts +3 -0
  7. src/engine-components/webxr/controllers/XRControllerModel.ts +15 -0
src/engine-components/DragControls.ts CHANGED
@@ -248,9 +248,14 @@
248
248
  const iterator = this._dragHandlers.values();
249
249
  const a = iterator.next().value;
250
250
  const b = iterator.next().value;
251
- const mtHandler = new MultiTouchDragHandler(this, this._targetObject!, a, b);
252
- this._dragHandlers.set(this.gameObject, mtHandler);
253
- mtHandler.onDragStart(args);
251
+ if (a instanceof DragPointerHandler && b instanceof DragPointerHandler) {
252
+ const mtHandler = new MultiTouchDragHandler(this, this._targetObject!, a, b);
253
+ this._dragHandlers.set(this.gameObject, mtHandler);
254
+ mtHandler.onDragStart(args);
255
+ }
256
+ else {
257
+ console.error("Attempting to construct a MultiTouchDragHandler with invalid DragPointerHandlers. This is likely a bug.", { a, b });
258
+ }
254
259
  }
255
260
 
256
261
  args.use();
@@ -605,6 +610,11 @@
605
610
  const draggedObject = this.gameObject;
606
611
  const targetObject = this._followObject;
607
612
 
613
+ if (!draggedObject) {
614
+ console.error("MultiTouchDragHandler has no dragged object. This is likely a bug.");
615
+ return;
616
+ }
617
+
608
618
  targetObject.updateMatrix();
609
619
  targetObject.updateMatrixWorld(true);
610
620
 
src/engine/engine_utils.ts CHANGED
@@ -630,6 +630,13 @@
630
630
  }
631
631
  }
632
632
 
633
+ let __isVisionOS: boolean | undefined;
634
+ /** @returns `true` for VisionOS devices */
635
+ export function isVisionOS() {
636
+ if (__isVisionOS !== undefined) return __isVisionOS;
637
+ return __isVisionOS = (isMacOS() && "xr" in navigator);
638
+ }
639
+
633
640
  let __isiOS: boolean | undefined;
634
641
  const iosDevices = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
635
642
 
src/engine/xr/NeedleXRController.ts CHANGED
@@ -161,7 +161,6 @@
161
161
  private _hasSelectEvent = false;
162
162
  get hasSelectEvent() { return this._hasSelectEvent; }
163
163
  private _isMxInk = false;
164
- private _isMxInkFallback = false;
165
164
  private _isMetaQuestTouchController = false;
166
165
 
167
166
  /** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
@@ -438,14 +437,6 @@
438
437
  rayPositionRaw = getTempVector(t.position);
439
438
  rayQuaternionRaw = getTempQuaternion(t.orientation);
440
439
 
441
- // HACK: offset for MX Ink on QuestOS v69 and less. This will hopefully not be required with OS v70+ anymore,
442
- // when the pen should have its own proper profile and correct ray space.
443
- if (this._isMxInk && !this._isMxInkFallback) {
444
- const offset = getTempVector(0.013, 0.000, -0.028).applyQuaternion(rayQuaternionRaw);
445
- rayPositionRaw.add(offset);
446
- this._rayPosition.add(offset);
447
- }
448
-
449
440
  this._rayPositionRaw.copy(rayPositionRaw);
450
441
  this._rayRotationRaw.copy(rayQuaternionRaw);
451
442
  }
@@ -759,11 +750,6 @@
759
750
  this.getMotionController = fetchProfileCall.then(res => {
760
751
 
761
752
  if (!this.connected) return null;
762
- if (this._isMxInk && !res.assetPath) {
763
- if (debug) console.log("Falling back to custom MX Ink model", res.profile, res.assetPath);
764
- res.assetPath = "https://cdn.needle.tools/static/models/controllers/logitech_vr_stylus_v1.3.1_grip_questos68.glb";
765
- this._isMxInkFallback = true;
766
- }
767
753
 
768
754
  this._motioncontroller = new MotionController(
769
755
  this.inputSource,
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -373,12 +373,20 @@
373
373
  static getDefaultSessionInit(mode: Omit<XRSessionMode, "inline">): XRSessionInit {
374
374
  switch (mode) {
375
375
  case "immersive-ar":
376
+ const arFeatures = ['anchors', 'local-floor', 'layers', 'dom-overlay', 'hit-test', 'unbounded'];
377
+ // Don't request handtracking by default on VisionOS
378
+ if (!DeviceUtilities.isVisionOS())
379
+ arFeatures.push('hand-tracking');
376
380
  return {
377
- optionalFeatures: ['anchors', 'local-floor', 'hand-tracking', 'layers', 'dom-overlay', 'hit-test', 'unbounded'],
381
+ optionalFeatures: arFeatures,
378
382
  }
379
383
  case "immersive-vr":
384
+ const vrFeatures = ['local-floor', 'bounded-floor', 'high-fixed-foveation-level', 'layers'];
385
+ // Don't request handtracking by default on VisionOS
386
+ if (!DeviceUtilities.isVisionOS())
387
+ vrFeatures.push('hand-tracking');
380
388
  return {
381
- optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'high-fixed-foveation-level', 'layers'],
389
+ optionalFeatures: vrFeatures,
382
390
  }
383
391
  default:
384
392
  console.warn("No default session init for mode", mode);
src/engine-components/webxr/WebXRPlaneTracking.ts CHANGED
@@ -364,7 +364,7 @@
364
364
  if (newPlane instanceof Mesh) {
365
365
  disposeObjectResources(newPlane.geometry);
366
366
  newPlane.geometry = this.createGeometry(data);
367
- this.makeOccluder(newPlane, newPlane.material, this.occluder);
367
+ this.makeOccluder(newPlane, newPlane.material, this.occluder && !this.dataTemplate);
368
368
  }
369
369
  else if (newPlane instanceof Group) {
370
370
  // We want to process only one level of children on purpose here
@@ -372,7 +372,7 @@
372
372
  if (ch instanceof Mesh) {
373
373
  disposeObjectResources(ch.geometry);
374
374
  ch.geometry = this.createGeometry(data);
375
- this.makeOccluder(ch, ch.material, this.occluder);
375
+ this.makeOccluder(ch, ch.material, this.occluder && !this.dataTemplate);
376
376
  }
377
377
  }
378
378
  }
src/engine-components/webxr/controllers/XRControllerFollow.ts CHANGED
@@ -71,6 +71,9 @@
71
71
 
72
72
  /** @internal */
73
73
  onUpdateXR(args: NeedleXREventArgs): void {
74
+ // explicit check since we're overriding activeAndEnabled
75
+ if (!this.enabled) return;
76
+
74
77
  // try to get the controller
75
78
  const ctrl = args.xr.getController(this.side);
76
79
  if (ctrl) {
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -113,6 +113,7 @@
113
113
  }
114
114
  }
115
115
  }
116
+
116
117
  onXRControllerRemoved(args: NeedleXRControllerEventArgs): void {
117
118
  console.debug("XR Controller Removed", args.controller.side, args.controller.index);
118
119
  // we need to find the index by the controller because if controller 0 is removed first then args.controller.index 1 will be at index 0
@@ -128,6 +129,19 @@
128
129
  entry.model = undefined;
129
130
  }
130
131
  }
132
+
133
+ onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): void {
134
+ // When a custom hand model is used, we want to ensure we're requesting hand tracking,
135
+ // even when the platform default doesn't include it (for example, on VisionOS we don't
136
+ // request hand tracking by default because there's an additional permissions dialogue.
137
+ if (this.createHandModel && (this.customLeftHand || this.customRightHand)) {
138
+ args.optionalFeatures = args.optionalFeatures || [];
139
+ if (!args.optionalFeatures.includes("hand-tracking")) {
140
+ args.optionalFeatures.push("hand-tracking");
141
+ }
142
+ }
143
+ }
144
+
131
145
  onLeaveXR(_args: NeedleXREventArgs): void {
132
146
  for (const entry of this._models) {
133
147
  if (!entry) continue;
@@ -145,6 +159,7 @@
145
159
  }
146
160
  this._models.length = 0;
147
161
  }
162
+
148
163
  onBeforeRender() {
149
164
  if (!NeedleXRSession.active) return;