@@ -65,12 +65,12 @@
|
|
65
65
|
@serializable(EventList)
|
66
66
|
onClick?: EventList;
|
67
67
|
|
68
|
-
private _isHovered:
|
68
|
+
private _isHovered: number = 0;
|
69
69
|
|
70
70
|
onPointerEnter(_) {
|
71
|
+
this._isHovered += 1;
|
71
72
|
if (debug)
|
72
|
-
console.
|
73
|
-
this._isHovered = true;
|
73
|
+
console.warn("Button Enter", this._isHovered, this.animationTriggers?.highlightedTrigger, this.animator);
|
74
74
|
if (!this.interactable) return;
|
75
75
|
if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
|
76
76
|
this.animator.setTrigger(this.animationTriggers.highlightedTrigger);
|
@@ -82,10 +82,12 @@
|
|
82
82
|
}
|
83
83
|
|
84
84
|
onPointerExit() {
|
85
|
+
this._isHovered -= 1;
|
85
86
|
if (debug)
|
86
|
-
console.log("Button Exit", this.animationTriggers?.highlightedTrigger, this.animator);
|
87
|
-
this._isHovered = false;
|
87
|
+
console.log("Button Exit", this._isHovered, this.animationTriggers?.highlightedTrigger, this.animator);
|
88
88
|
if (!this.interactable) return;
|
89
|
+
if (this._isHovered > 0) return;
|
90
|
+
this._isHovered = 0;
|
89
91
|
if (this.transition == Transition.Animation && this.animationTriggers && this.animator) {
|
90
92
|
this.animator.setTrigger(this.animationTriggers.normalTrigger);
|
91
93
|
}
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { Mathf } from "../../engine/engine_math.js";
|
5
5
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
6
6
|
import { FrameEvent } from "../../engine/engine_setup.js";
|
7
|
-
import { getParam } from "../../engine/engine_utils.js";
|
7
|
+
import { delayForFrames, getParam } from "../../engine/engine_utils.js";
|
8
8
|
import { NeedleXREventArgs } from "../../engine/xr/index.js";
|
9
9
|
import { Camera } from "../Camera.js";
|
10
10
|
import { GameObject } from "../Component.js";
|
@@ -202,12 +202,19 @@
|
|
202
202
|
}
|
203
203
|
}
|
204
204
|
|
205
|
-
onEnterXR(args: NeedleXREventArgs)
|
205
|
+
async onEnterXR(args: NeedleXREventArgs) {
|
206
|
+
// workaround for https://linear.app/needle/issue/NE-4114
|
206
207
|
if (this.screenspace) {
|
207
208
|
if (args.xr.isVR || args.xr.isPassThrough) {
|
208
209
|
this.gameObject.visible = false;
|
209
210
|
}
|
210
211
|
}
|
212
|
+
else {
|
213
|
+
this.gameObject.visible = false;
|
214
|
+
await delayForFrames(1).then(()=>{
|
215
|
+
this.gameObject.visible = true;
|
216
|
+
});
|
217
|
+
}
|
211
218
|
}
|
212
219
|
onLeaveXR(args: NeedleXREventArgs): void {
|
213
220
|
if (this.screenspace) {
|
@@ -451,6 +451,16 @@
|
|
451
451
|
private _initialDistance: number = 0;
|
452
452
|
|
453
453
|
private alignManipulator() {
|
454
|
+
if (!this.handlerA || !this.handlerB) {
|
455
|
+
console.error("alignManipulator called on MultiTouchDragHandler without valid handlers. This is likely a bug.", this);
|
456
|
+
return;
|
457
|
+
}
|
458
|
+
|
459
|
+
if (!this.handlerA.followObject || !this.handlerB.followObject) {
|
460
|
+
console.error("alignManipulator called on MultiTouchDragHandler without valid follow objects. This is likely a bug.", this.handlerA, this.handlerB);
|
461
|
+
return;
|
462
|
+
}
|
463
|
+
|
454
464
|
this._tempVec1.copy(this._handlerAAttachmentPoint);
|
455
465
|
this._tempVec2.copy(this._handlerBAttachmentPoint);
|
456
466
|
this.handlerA.followObject.localToWorld(this._tempVec1);
|
@@ -1005,7 +1005,7 @@
|
|
1005
1005
|
if ((isDevEnvironment() || debug) && (err instanceof Error || err instanceof TypeError))
|
1006
1006
|
showBalloonMessage(`Caught unhandled exception during render-loop - see console for details.`, LogType.Error);
|
1007
1007
|
console.error("Frame #" + this.time.frame + "\n", err);
|
1008
|
-
if (this._renderlooperrors
|
1008
|
+
if (this._renderlooperrors >= 3) {
|
1009
1009
|
console.warn("Stopping render loop due to error")
|
1010
1010
|
this.renderer.setAnimationLoop(null);
|
1011
1011
|
}
|
@@ -208,12 +208,12 @@
|
|
208
208
|
width: "auto",
|
209
209
|
fontSize: size,
|
210
210
|
color: color,
|
211
|
-
lineHeight:
|
211
|
+
lineHeight: 1,
|
212
212
|
backgroundColor: backgroundColor ?? undefined,
|
213
213
|
backgroundOpacity: opacity,
|
214
214
|
textContent: text,
|
215
|
-
borderRadius:
|
216
|
-
padding:
|
215
|
+
borderRadius: .5 * size,
|
216
|
+
padding: .8 * size,
|
217
217
|
whiteSpace: 'pre',
|
218
218
|
};
|
219
219
|
|
@@ -232,7 +232,6 @@
|
|
232
232
|
// handle.setText(text);
|
233
233
|
}
|
234
234
|
this.tmuiNeedsUpdate = true;
|
235
|
-
element.layers.enableAll();
|
236
235
|
this.registerTimedObject(Context.Current, element as any, duration, this.textLabelCache as any);
|
237
236
|
return element as Text & LabelHandle;
|
238
237
|
}
|
@@ -320,8 +319,10 @@
|
|
320
319
|
context.post_render_callbacks.push(postRender);
|
321
320
|
}
|
322
321
|
|
323
|
-
object.
|
324
|
-
|
322
|
+
object.traverse(obj => {
|
323
|
+
obj.layers.disableAll();
|
324
|
+
obj.layers.enable(2);
|
325
|
+
});
|
325
326
|
|
326
327
|
object.renderOrder = 999999;
|
327
328
|
object[$cacheSymbol] = cache;
|
@@ -349,7 +350,7 @@
|
|
349
350
|
continue;
|
350
351
|
}
|
351
352
|
const isInXR = ctx.isInVR;
|
352
|
-
const keepUp =
|
353
|
+
const keepUp = false;
|
353
354
|
const copyRotation = !isInXR;
|
354
355
|
lookAtObject(obj as any, ctx.mainCamera, keepUp, copyRotation);
|
355
356
|
}
|
@@ -364,7 +365,7 @@
|
|
364
365
|
objects.splice(i, 1);
|
365
366
|
times.splice(i, 1);
|
366
367
|
obj.removeFromParent();
|
367
|
-
if (isDestroyed(obj)
|
368
|
+
if (isDestroyed(obj) != true) {
|
368
369
|
const cache = obj[$cacheSymbol];
|
369
370
|
cache.push(obj);
|
370
371
|
}
|
@@ -526,5 +526,7 @@
|
|
526
526
|
|
527
527
|
export type XRControllerButtonName = "thumbrest" | "xr-standard-trigger" | "xr-standard-squeeze" | "xr-standard-thumbstick" | "xr-standard-touchpad" | "menu" | GamepadButtonName;
|
528
528
|
|
529
|
+
export type XRGestureName = "pinch";
|
530
|
+
|
529
531
|
/** All known (types) button names for various devices and cases combined. You should use the device specific names if you e.g. know you only deal with a mouse use MouseButtonName */
|
530
|
-
export type ButtonName = "unknown" | MouseButtonName | GamepadButtonName | XRControllerButtonName;
|
532
|
+
export type ButtonName = "unknown" | MouseButtonName | GamepadButtonName | XRControllerButtonName | XRGestureName;
|
@@ -28,6 +28,8 @@
|
|
28
28
|
hasActiveUI: boolean
|
29
29
|
}
|
30
30
|
|
31
|
+
declare type IComponentCanMaybeReceiveEvents = IPointerEventHandler & IComponent & { interactable?: boolean };
|
32
|
+
|
31
33
|
export class EventSystem extends Behaviour {
|
32
34
|
private static _eventSystemMap = new Map<Context, EventSystem[]>();
|
33
35
|
|
@@ -340,7 +342,7 @@
|
|
340
342
|
// thus is not hovering over anything
|
341
343
|
const hoveredData = this.hoveredByID.get(args.pointerId);
|
342
344
|
if (hoveredData) {
|
343
|
-
this.
|
345
|
+
this.propagatePointerExit(hoveredData.obj, hoveredData.data, null);
|
344
346
|
}
|
345
347
|
this.hoveredByID.delete(args.pointerId);
|
346
348
|
|
@@ -444,7 +446,7 @@
|
|
444
446
|
|
445
447
|
// trigger onPointerExit
|
446
448
|
if (isNewlyHovering && prevHovering) {
|
447
|
-
this.
|
449
|
+
this.propagatePointerExit(prevHovering, hovering.data, object);
|
448
450
|
}
|
449
451
|
|
450
452
|
// save hovered object
|
@@ -513,35 +515,29 @@
|
|
513
515
|
case "hand":
|
514
516
|
// for hands and controller we assume they are never totally still (except for simulated environments)
|
515
517
|
// we might want to add a threshold here (e.g. if a user holds their hand very still or controller)
|
516
|
-
// so maybe check the angle
|
518
|
+
// so maybe check the angle everxy frame?
|
517
519
|
break;
|
518
520
|
}
|
519
521
|
|
520
522
|
this.propagate(object, (behaviour) => {
|
521
|
-
const comp = behaviour as
|
523
|
+
const comp = behaviour as IComponentCanMaybeReceiveEvents;
|
522
524
|
|
523
525
|
if (comp.interactable === false) return;
|
524
526
|
if (!comp.activeAndEnabled || !comp.enabled) return;
|
525
527
|
|
526
528
|
if (comp.onPointerEnter) {
|
527
529
|
if (hoveredObjectChanged) {
|
528
|
-
|
529
|
-
comp[this.pointerEnterSymbol] = true;
|
530
|
-
delete comp[this.pointerExitSymbol];
|
531
|
-
comp.onPointerEnter(args);
|
532
|
-
}
|
530
|
+
this.handlePointerEnter(comp, args);
|
533
531
|
}
|
534
532
|
}
|
535
533
|
|
536
534
|
if (args.isDown) {
|
537
535
|
if (comp.onPointerDown) {
|
538
536
|
comp.onPointerDown(args);
|
539
|
-
|
540
537
|
// Set the handler that we called the down event on
|
541
538
|
// So we can call the up event on the same handler
|
542
539
|
// In a scenario where we Down on one object and Up on another
|
543
540
|
pressedEvent?.handlers.add(comp);
|
544
|
-
|
545
541
|
this.handlePointerCapture(args, comp);
|
546
542
|
}
|
547
543
|
}
|
@@ -582,11 +578,8 @@
|
|
582
578
|
}
|
583
579
|
}
|
584
580
|
|
585
|
-
/**
|
586
|
-
|
587
|
-
*/
|
588
|
-
private triggerOnExit(object: Object3D, args: PointerEventData, newObject: Object3D | null) {
|
589
|
-
|
581
|
+
/** Propagate up in hierarchy and call onPointerExit */
|
582
|
+
private propagatePointerExit(object: Object3D, args: PointerEventData, newObject: Object3D | null) {
|
590
583
|
this.propagate(object, (behaviour) => {
|
591
584
|
if (!behaviour.gameObject || behaviour.destroyed) return;
|
592
585
|
|
@@ -596,10 +589,7 @@
|
|
596
589
|
if (newObject && this.isChild(newObject, behaviour.gameObject)) {
|
597
590
|
return;
|
598
591
|
}
|
599
|
-
|
600
|
-
inst[this.pointerExitSymbol] = true;
|
601
|
-
delete inst[this.pointerEnterSymbol];
|
602
|
-
inst.onPointerExit(args);
|
592
|
+
this.handlePointerExit(inst, args);
|
603
593
|
}
|
604
594
|
});
|
605
595
|
}
|
@@ -610,6 +600,53 @@
|
|
610
600
|
this.releasePointerCapture(evt, handler);
|
611
601
|
}
|
612
602
|
|
603
|
+
/** Responsible for invoking onPointerEnter (and updating onPointerExit). We invoke onPointerEnter once per active pointerId */
|
604
|
+
private handlePointerEnter(comp: IComponentCanMaybeReceiveEvents, args: PointerEventData) {
|
605
|
+
if (comp.onPointerEnter) {
|
606
|
+
if (this.updatePointerState(comp, args.pointerId, this.pointerEnterSymbol, true)) {
|
607
|
+
comp.onPointerEnter(args);
|
608
|
+
}
|
609
|
+
}
|
610
|
+
this.updatePointerState(comp, args.pointerId, this.pointerExitSymbol, false);
|
611
|
+
}
|
612
|
+
|
613
|
+
/** Responsible for invoking onPointerExit (and updating onPointerEnter). We invoke onPointerExit once per active pointerId */
|
614
|
+
private handlePointerExit(comp: IComponentCanMaybeReceiveEvents, evt: PointerEventData) {
|
615
|
+
if (comp.onPointerExit) {
|
616
|
+
if (this.updatePointerState(comp, evt.pointerId, this.pointerExitSymbol, true)) {
|
617
|
+
comp.onPointerExit(evt);
|
618
|
+
}
|
619
|
+
}
|
620
|
+
this.updatePointerState(comp, evt.pointerId, this.pointerEnterSymbol, false);
|
621
|
+
}
|
622
|
+
|
623
|
+
/** updates the pointer state list for a component
|
624
|
+
* @param comp the component to update
|
625
|
+
* @param pointerId the pointerId to update
|
626
|
+
* @param symbol the symbol to use for the state
|
627
|
+
* @param add if true, the pointerId is added to the state list, if false the pointerId will be removed
|
628
|
+
*/
|
629
|
+
private updatePointerState(comp: IComponentCanMaybeReceiveEvents, pointerId: number, symbol: symbol, add: boolean) {
|
630
|
+
let state = comp[symbol];
|
631
|
+
|
632
|
+
if (add) {
|
633
|
+
// the pointer is already in the state list
|
634
|
+
if (state && state.includes(pointerId)) return false;
|
635
|
+
state = state || [];
|
636
|
+
state.push(pointerId);
|
637
|
+
comp[symbol] = state;
|
638
|
+
return true;
|
639
|
+
}
|
640
|
+
else {
|
641
|
+
if (!state || !state.includes(pointerId)) return false;
|
642
|
+
const i = state.indexOf(pointerId);
|
643
|
+
if (i !== -1) {
|
644
|
+
state.splice(i, 1);
|
645
|
+
}
|
646
|
+
return true;
|
647
|
+
}
|
648
|
+
}
|
649
|
+
|
613
650
|
/** the list of component handlers that requested pointerCapture for a specific pointerId */
|
614
651
|
private readonly _capturedPointer: { [id: number]: IPointerEventHandler[] } = {};
|
615
652
|
|
@@ -1,17 +1,26 @@
|
|
1
1
|
import { fetchProfile, MotionController } from "@webxr-input-profiles/motion-controllers";
|
2
2
|
import { AxesHelper, Int8BufferAttribute, Object3D, Quaternion, Ray, Vector3 } from "three";
|
3
3
|
|
4
|
+
import { RGBAColor } from "../../engine-components/js-extensions/RGBAColor.js";
|
4
5
|
import { Context } from "../engine_context.js";
|
5
6
|
import { Gizmos } from "../engine_gizmos.js";
|
6
7
|
import { InputEventNames, InputEvents, NEPointerEvent, NEPointerEventInit, PointerType } from "../engine_input.js";
|
7
8
|
import { getTempQuaternion, getTempVector, getWorldQuaternion } from "../engine_three_utils.js";
|
8
|
-
import type { ButtonName, IGameObject, Vec3, XRControllerButtonName } from "../engine_types.js";
|
9
|
+
import type { ButtonName, IGameObject, Vec3, XRControllerButtonName, XRGestureName } from "../engine_types.js";
|
9
10
|
import { getParam } from "../engine_utils.js";
|
10
11
|
import { flipForwardMatrix, flipForwardQuaternion } from "./internal.js";
|
11
12
|
import type { NeedleXRHitTestResult, NeedleXRSession } from "./NeedleXRSession.js";
|
12
13
|
|
13
14
|
const debug = getParam("debugwebxr");
|
15
|
+
/** when enabled we will not use the browser select event but instead
|
16
|
+
* we will emit the input event based on our own pinch detection
|
17
|
+
* this is a workaround for visionOS not emitting the select events, see https://linear.app/needle/issue/NE-4212
|
18
|
+
*/
|
19
|
+
const debugCustomGesture = getParam("debugcustomgesture");
|
14
20
|
|
21
|
+
/** true when selectstart was ever received */
|
22
|
+
let _didReceiveSelectStartEvent = false;
|
23
|
+
|
15
24
|
// https://github.com/immersive-web/webxr-input-profiles/blob/4484a05e30bcd43fe86bb4e06b7a707861a26796/packages/registry/profiles/meta/meta-quest-touch-plus.json
|
16
25
|
declare type ControllerAxes = "xr-standard-thumbstick";
|
17
26
|
declare type StickName = "xr-standard-thumbstick";
|
@@ -62,6 +71,7 @@
|
|
62
71
|
export class NeedleXRController {
|
63
72
|
/** the Needle XR Session */
|
64
73
|
readonly xr: NeedleXRSession;
|
74
|
+
get context() { return this.xr.context; }
|
65
75
|
/**
|
66
76
|
* https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource
|
67
77
|
*/
|
@@ -90,6 +100,10 @@
|
|
90
100
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRHand
|
91
101
|
*/
|
92
102
|
get hand() { return this.inputSource.hand; }
|
103
|
+
/** threejs XRHandSpace, shorthand for `context.renderer.xr.getHand(controllerIndex)`
|
104
|
+
* @link https://threejs.org/docs/#api/en/renderers/webxr/WebXRManager.getHand
|
105
|
+
*/
|
106
|
+
get handObject() { return this.context.renderer.xr.getHand(this.index); }
|
93
107
|
/** The input source profiles */
|
94
108
|
get profiles() { return this.inputSource.profiles; }
|
95
109
|
/** The device input layout */
|
@@ -200,7 +214,7 @@
|
|
200
214
|
get object() { return this._object; }
|
201
215
|
private readonly _object: IGameObject;
|
202
216
|
|
203
|
-
private readonly _debugAxesHelper = new AxesHelper(.
|
217
|
+
private readonly _debugAxesHelper = new AxesHelper(.2);
|
204
218
|
|
205
219
|
/** returns the URL of the default controller model */
|
206
220
|
async getModelUrl(): Promise<string | null> {
|
@@ -250,6 +264,13 @@
|
|
250
264
|
onRenderDebug() {
|
251
265
|
Gizmos.DrawWireSphere(this.rayWorldPosition, .02);
|
252
266
|
Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, 0, 10).applyQuaternion(this.rayWorldQuaternion));
|
267
|
+
const debugLabelPosition = this.gripWorldPosition.sub(this.object.worldForward.multiplyScalar(.1));
|
268
|
+
const profileStr = this.inputSource.profiles.join("\n");
|
269
|
+
let debugStr = `Controller[${this.index}] ${this.side}
|
270
|
+
C:${this.connected ? "yes" : "no"} T:${this.isTracking ? "yes" : "no"} Hand:${this.inputSource.hand ? "yes" : "no"}`;
|
271
|
+
if (this.inputSource.hand) debugStr += `\nPinch: ${this.getGesture("pinch")?.value.toFixed(3)}`;
|
272
|
+
debugStr += "\n" + profileStr;
|
273
|
+
Gizmos.DrawLabel(debugLabelPosition, debugStr, .01);
|
253
274
|
}
|
254
275
|
|
255
276
|
private onUpdateFrame(frame: XRFrame) {
|
@@ -345,7 +366,7 @@
|
|
345
366
|
|
346
367
|
/** Called when the input source disconnects */
|
347
368
|
onDisconnected() {
|
348
|
-
if (this.
|
369
|
+
if (debug) console.warn("Controller disconnected", this.index);
|
349
370
|
// move all attached objects into the scene
|
350
371
|
for (const child of this._object.children) {
|
351
372
|
this.xr.context.scene.attach(child);
|
@@ -398,7 +419,21 @@
|
|
398
419
|
return undefined;
|
399
420
|
}
|
400
421
|
|
401
|
-
|
422
|
+
/** Get a gesture state */
|
423
|
+
getGesture(key: XRGestureName): NeedleGamepadButton | null {
|
424
|
+
const state = this.states[key];
|
425
|
+
if (!state) return null;
|
426
|
+
this.states[key] = state;
|
427
|
+
const needleButton = this._needleGamepadButtons[key] || new NeedleGamepadButton();
|
428
|
+
needleButton.pressed = state.pressed;
|
429
|
+
needleButton.value = state.value;
|
430
|
+
needleButton.isDown = state.isDown;
|
431
|
+
needleButton.isUp = state.isUp;
|
432
|
+
this._needleGamepadButtons[key] = needleButton;
|
433
|
+
return needleButton;
|
434
|
+
}
|
435
|
+
|
436
|
+
private readonly _needleGamepadButtons: { [key: number | string]: NeedleGamepadButton } = {};
|
402
437
|
/** combine the InputState information + the GamepadButton information (since GamepadButtons can not be extended) */
|
403
438
|
private toNeedleGamepadButton(index: number): NeedleGamepadButton {
|
404
439
|
const button = this.inputSource.gamepad?.buttons[index];
|
@@ -453,7 +488,6 @@
|
|
453
488
|
return { x: 0, y: 0, z: 0 }
|
454
489
|
}
|
455
490
|
|
456
|
-
|
457
491
|
private readonly _buttonMap = new Map<ButtonName, number>();
|
458
492
|
|
459
493
|
// the motion controller contains the controller scheme, we use this to simplify button access
|
@@ -520,10 +554,21 @@
|
|
520
554
|
const selectComponentId = this._layout?.selectComponentId;
|
521
555
|
const i = this._layout?.components[selectComponentId!]?.gamepadIndices?.button;
|
522
556
|
if (i !== undefined) this._selectButtonIndex = i;
|
557
|
+
if (debugCustomGesture) return;
|
558
|
+
if (!_didReceiveSelectStartEvent) {
|
559
|
+
_didReceiveSelectStartEvent = true;
|
560
|
+
// safeguard first pinch event - check if the pinch gesture is already down
|
561
|
+
const pinch = this.getGesture("pinch");
|
562
|
+
if (pinch?.pressed) {
|
563
|
+
console.warn("Select start event was received but the pinch gesture is already down. This might happen the first time you start pinching", this.index, this.side);
|
564
|
+
return;
|
565
|
+
}
|
566
|
+
}
|
523
567
|
if (debug) Gizmos.DrawDirection(this.rayWorldPosition, getTempVector(0, .01, 1).applyQuaternion(this.rayWorldQuaternion), 0xff0000, 10);
|
524
568
|
this.emitPointerEvent(InputEvents.PointerDown, this._selectButtonIndex || 0, "xr-standard-trigger", true, evt);
|
525
569
|
}
|
526
570
|
private onSelectEnd = (evt: XRInputSourceEvent) => {
|
571
|
+
if (debugCustomGesture) return;
|
527
572
|
if (this.inputSource !== evt.inputSource) return;
|
528
573
|
this.emitPointerEvent(InputEvents.PointerUp, this._selectButtonIndex || 0, "xr-standard-trigger", true, evt);
|
529
574
|
}
|
@@ -542,7 +587,7 @@
|
|
542
587
|
};
|
543
588
|
|
544
589
|
/** Index = button index */
|
545
|
-
private readonly states
|
590
|
+
private readonly states: { [key: number | string]: InputState } = {};
|
546
591
|
// If we want to invoke button events for ALL buttons we need to keep track of the previous state
|
547
592
|
// instead of using XR input select start events which is only raised for the primary button
|
548
593
|
// we should probably do both but then we need to ignore the primary index in the following function (to not raise an event for the same button twice)
|
@@ -583,11 +628,65 @@
|
|
583
628
|
|
584
629
|
if (eventName != null && emitEvent) {
|
585
630
|
const name = this._layout?.gamepad[k];
|
631
|
+
if (debug || debugCustomGesture) console.log("Emitting pointer event", eventName, k, name, button.value, this.gamepad, this._layout);
|
586
632
|
this.emitPointerEvent(eventName, k, name ?? "none", false, null, button.value);
|
587
633
|
}
|
588
634
|
}
|
589
635
|
}
|
636
|
+
|
637
|
+
// update hand gesture states
|
638
|
+
if (this.hand) {
|
639
|
+
const handObject = this.handObject;
|
640
|
+
if (handObject) {
|
641
|
+
// update pinch state
|
642
|
+
const indexTip = handObject.joints["index-finger-tip"];
|
643
|
+
const thumbTip = handObject.joints["thumb-tip"];
|
644
|
+
if (indexTip && thumbTip) {
|
645
|
+
const pinchThreshold = .02;
|
646
|
+
const pinchHysteresis = .01;
|
647
|
+
const distance = indexTip.position.distanceTo(thumbTip.position);
|
648
|
+
const state = this.states["pinch"] || new InputState();
|
649
|
+
state.value = distance;
|
650
|
+
|
651
|
+
const isPressed = distance < (pinchThreshold - pinchHysteresis);
|
652
|
+
const isReleased = distance > (pinchThreshold + pinchHysteresis);
|
653
|
+
if (isPressed && !state.pressed) {
|
654
|
+
if (debugCustomGesture) console.log("pinch start", distance);
|
655
|
+
state.isDown = true;
|
656
|
+
state.isUp = false;
|
657
|
+
state.pressed = true;
|
658
|
+
}
|
659
|
+
else if (isReleased && state.pressed) {
|
660
|
+
state.isDown = false;
|
661
|
+
state.isUp = true;
|
662
|
+
state.pressed = false;
|
663
|
+
}
|
664
|
+
else {
|
665
|
+
state.isDown = false;
|
666
|
+
state.isUp = false;
|
667
|
+
}
|
668
|
+
this.states["pinch"] = state;
|
669
|
+
|
670
|
+
/** workaround for visionOS not emitting selectstart. See https://linear.app/needle/issue/NE-4212
|
671
|
+
* If a select start event was never received we do a manual check here if the user is pinching
|
672
|
+
*/
|
673
|
+
if (!_didReceiveSelectStartEvent && (state.isDown || state.isUp)) {
|
674
|
+
const eventName = isPressed ? "pointerdown" : "pointerup";
|
675
|
+
const pressure = distance / pinchThreshold;
|
676
|
+
if (debugCustomGesture) {
|
677
|
+
const p = this.rayWorldPosition.add(this.object.worldForward.multiplyScalar(.2));
|
678
|
+
p.y += .05;
|
679
|
+
p.y += Math.random() * .02;
|
680
|
+
Gizmos.DrawLabel(p, "pinch:" + eventName + ", " + this.index + ", " + this.side + "\n" + handObject.uuid, 0.01, 5, 0x000000, new RGBAColor(1, 1, 1, .1));
|
681
|
+
}
|
682
|
+
this.emitPointerEvent(eventName, 0, "pinch", false, null, pressure);
|
683
|
+
}
|
684
|
+
}
|
685
|
+
}
|
686
|
+
}
|
590
687
|
}
|
688
|
+
|
689
|
+
|
591
690
|
private onUpdateMove() {
|
592
691
|
let button = this.xr.context.input.getFirstPressedButtonForPointer(this.index);
|
593
692
|
if (button === undefined) button = 0;
|
@@ -622,6 +721,7 @@
|
|
622
721
|
|
623
722
|
const prevContext = Context.Current;
|
624
723
|
Context.Current = this.xr.context;
|
724
|
+
if (debug && type !== "pointermove") console.warn("Pointer event", type, button, buttonName, { ...this.pointerInit });
|
625
725
|
this.xr.context.input.createInputEvent(new NEPointerEvent(type, source, this.pointerInit));
|
626
726
|
Context.Current = prevContext;
|
627
727
|
}
|
@@ -1081,7 +1081,9 @@
|
|
1081
1081
|
|
1082
1082
|
if ( geometry ) {
|
1083
1083
|
writer.beginBlock( `def ${objType} "${name}"`, "(", false );
|
1084
|
-
|
1084
|
+
// NE-4084: To use the doubleSided workaround with skeletal meshes we'd have to
|
1085
|
+
// also emit extra data for jointIndices etc., so we're skipping skinned meshes here.
|
1086
|
+
if (context.quickLookCompatible && material && material.side === DoubleSide && !isSkinnedMesh)
|
1085
1087
|
writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry_doubleSided>`);
|
1086
1088
|
else
|
1087
1089
|
writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>`);
|
@@ -6,6 +6,7 @@
|
|
6
6
|
import { showBalloonWarning } from "../../../engine/debug/index.js";
|
7
7
|
import { AssetReference } from "../../../engine/engine_addressables.js";
|
8
8
|
import { setDontDestroy } from "../../../engine/engine_gameobject.js";
|
9
|
+
import { Gizmos } from "../../../engine/engine_gizmos.js";
|
9
10
|
import { addDracoAndKTX2Loaders } from "../../../engine/engine_loaders.js";
|
10
11
|
import { serializable } from "../../../engine/engine_serialization_decorator.js";
|
11
12
|
import { IGameObject } from "../../../engine/engine_types.js";
|
@@ -58,7 +59,8 @@
|
|
58
59
|
res?.handmesh?.controller?.removeFromParent();
|
59
60
|
return;
|
60
61
|
}
|
61
|
-
this._models
|
62
|
+
this._models.push({ controller: controller, model: res.handObject, handmesh: res.handmesh });
|
63
|
+
this._models.sort((a, b) => a.controller.index - b.controller.index);
|
62
64
|
this.scene.add(res.handObject);
|
63
65
|
}
|
64
66
|
}
|
@@ -68,7 +70,8 @@
|
|
68
70
|
if (assetUrl) {
|
69
71
|
const model = await this.loadModel(controller, assetUrl);
|
70
72
|
if (!model || !controller.connected) return;
|
71
|
-
this._models
|
73
|
+
this._models.push({ controller: controller, model });
|
74
|
+
this._models.sort((a, b) => a.controller.index - b.controller.index);
|
72
75
|
this.scene.add(model);
|
73
76
|
// The controller mesh should by default inherit layers.
|
74
77
|
model.traverse(child => {
|
@@ -187,6 +190,10 @@
|
|
187
190
|
|
188
191
|
const context = this.context;
|
189
192
|
const hand = context.renderer.xr.getHand(controller.index);
|
193
|
+
if (!hand) {
|
194
|
+
if (debug) Gizmos.DrawLabel(controller.rayWorldPosition, "No hand found for index " + controller.index, .05, 5);
|
195
|
+
else console.warn("No hand found for index " + controller.index);
|
196
|
+
}
|
190
197
|
|
191
198
|
const loader = new GLTFLoader();
|
192
199
|
addDracoAndKTX2Loaders(loader, context);
|
@@ -220,6 +227,7 @@
|
|
220
227
|
this.makeOccluder(child);
|
221
228
|
});
|
222
229
|
if (!controller.connected) {
|
230
|
+
if(debug) Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
|
223
231
|
object.removeFromParent();
|
224
232
|
}
|
225
233
|
});
|
@@ -229,9 +237,7 @@
|
|
229
237
|
if (controller.inputSource.hand) {
|
230
238
|
if (debug) console.log(controller.inputSource.hand);
|
231
239
|
for (const inputjoint of controller.inputSource.hand.values()) {
|
232
|
-
|
233
240
|
if (hand.joints[inputjoint.jointName] === undefined) {
|
234
|
-
|
235
241
|
const joint = new Group();
|
236
242
|
joint.matrixAutoUpdate = false;
|
237
243
|
joint.visible = true;
|
@@ -243,6 +249,12 @@
|
|
243
249
|
}
|
244
250
|
}
|
245
251
|
}
|
252
|
+
else {
|
253
|
+
if(debug) {
|
254
|
+
Gizmos.DrawLabel(controller.rayWorldPosition, "No inputSource.hand found for index " + controller.index, .05, 5);
|
255
|
+
}
|
256
|
+
}
|
257
|
+
|
246
258
|
return { handObject: handObject as IGameObject, handmesh: handmesh };
|
247
259
|
}
|
248
260
|
|
@@ -122,10 +122,6 @@
|
|
122
122
|
const dir = rotationInput > 0 ? 1 : -1;
|
123
123
|
rig.rotateY(dir * Mathf.toRadians(this.rotationStep));
|
124
124
|
}
|
125
|
-
|
126
|
-
const pos = controller.rayWorldPosition;
|
127
|
-
pos.y += .1
|
128
|
-
if (debug) Gizmos.DrawLabel(pos, stick.x.toFixed(2) + ", " + stick.y.toFixed(2), .02, 0)
|
129
125
|
}
|
130
126
|
|
131
127
|
protected onHandleTeleport(controller: NeedleXRController, rig: IGameObject) {
|
@@ -166,20 +162,18 @@
|
|
166
162
|
|
167
163
|
protected renderRays(session: NeedleXRSession) {
|
168
164
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
line.visible = false;
|
173
|
-
}
|
165
|
+
for (let i = 0; i < this._lines.length; i++) {
|
166
|
+
const line = this._lines[i];
|
167
|
+
line.visible = false;
|
174
168
|
}
|
175
169
|
|
176
|
-
for (const disc of this._hitDiscs) {
|
177
|
-
if (disc) disc.visible = false;
|
178
|
-
}
|
179
|
-
|
180
170
|
for (let i = 0; i < session.controllers.length; i++) {
|
181
171
|
const ctrl = session.controllers[i];
|
182
172
|
let line = this._lines[i];
|
173
|
+
if (!ctrl.connected || !ctrl.isTracking) {
|
174
|
+
if (line) line.visible = false;
|
175
|
+
continue;
|
176
|
+
}
|
183
177
|
if (!line) {
|
184
178
|
line = this.createRayLineObject();
|
185
179
|
line.scale.z = .5;
|
@@ -209,6 +203,7 @@
|
|
209
203
|
}
|
210
204
|
for (let i = 0; i < session.controllers.length; i++) {
|
211
205
|
const ctrl = session.controllers[i];
|
206
|
+
if (!ctrl.connected || !ctrl.isTracking) continue;
|
212
207
|
const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter })[0];
|
213
208
|
this._hitDistances[i] = hit?.distance;
|
214
209
|
if (hit) {
|