@@ -25,22 +25,38 @@
|
|
25
25
|
|
26
26
|
export const $shadowDomOwner = Symbol("shadowDomOwner");
|
27
27
|
|
28
|
+
/** Derive from this class if you want to implement your own UI components
|
29
|
+
* It provides utility methods and simplifies managing the underlying three-mesh-ui hierarchy
|
30
|
+
*/
|
28
31
|
export class BaseUIComponent extends Behaviour {
|
29
32
|
|
33
|
+
/** Is this object on the root of the UI hierarchy ? */
|
30
34
|
isRoot() { return this.Root?.gameObject === this.gameObject; }
|
31
35
|
|
36
|
+
/** Access the parent canvas component */
|
32
37
|
get canvas() {
|
33
38
|
const cv = this.Root as any as ICanvas;
|
34
39
|
if (cv?.isCanvas) return cv;
|
35
40
|
return null;
|
36
41
|
}
|
42
|
+
/** @deprecated use `canvas` */
|
43
|
+
protected get Canvas() {
|
44
|
+
return this.canvas;
|
45
|
+
}
|
37
46
|
|
47
|
+
/** Mark the UI dirty which will trigger an THREE-Mesh-UI update */
|
38
48
|
markDirty() {
|
39
49
|
EventSystem.markUIDirty(this.context);
|
40
50
|
}
|
41
51
|
|
42
|
-
|
52
|
+
/** the underlying three-mesh-ui */
|
53
|
+
get shadowComponent() { return this._shadowComponent }
|
54
|
+
private set shadowComponent(val: Object3D | null) {
|
55
|
+
this._shadowComponent = val;
|
56
|
+
}
|
43
57
|
|
58
|
+
private _shadowComponent: Object3D | null = null;
|
59
|
+
|
44
60
|
private _controlsChildLayout = true;
|
45
61
|
get controlsChildLayout(): boolean { return this._controlsChildLayout; }
|
46
62
|
set controlsChildLayout(val: boolean) {
|
@@ -59,11 +75,6 @@
|
|
59
75
|
return this._root;
|
60
76
|
}
|
61
77
|
|
62
|
-
// TODO: rename to canvas
|
63
|
-
protected get Canvas() {
|
64
|
-
return this.canvas;
|
65
|
-
}
|
66
|
-
|
67
78
|
// private _intermediate?: Object3D;
|
68
79
|
protected _parentComponent?: BaseUIComponent | null = undefined;
|
69
80
|
|
@@ -78,7 +89,10 @@
|
|
78
89
|
super.onEnable();
|
79
90
|
}
|
80
91
|
|
81
|
-
|
92
|
+
/** Add a three-mesh-ui object to the UI hierarchy
|
93
|
+
* @param container the three-mesh-ui object to add
|
94
|
+
* @param parent the parent component to add the object to
|
95
|
+
*/
|
82
96
|
protected addShadowComponent(container: any, parent?: BaseUIComponent) {
|
83
97
|
|
84
98
|
this.removeShadowComponent();
|
@@ -135,20 +149,6 @@
|
|
135
149
|
if(debug) console.log(this.shadowComponent)
|
136
150
|
}
|
137
151
|
|
138
|
-
|
139
|
-
set(_state: object) {
|
140
|
-
// if (!this.shadowComponent) return;
|
141
|
-
// this.traverseOwnedShadowComponents(this.shadowComponent, this, o => {
|
142
|
-
// for (const ch of o.children) {
|
143
|
-
// console.log(this, ch);
|
144
|
-
// if (ch.isUI && typeof ch.set === "function") {
|
145
|
-
// // ch.set(state);
|
146
|
-
// // ch.update(true, true, true);
|
147
|
-
// }
|
148
|
-
// }
|
149
|
-
// })
|
150
|
-
}
|
151
|
-
|
152
152
|
protected setShadowComponentOwner(current: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) {
|
153
153
|
if (!current) return;
|
154
154
|
// TODO: only traverse our own hierarchy, we can stop if we find another owner
|
@@ -172,6 +172,7 @@
|
|
172
172
|
}
|
173
173
|
}
|
174
174
|
|
175
|
+
/** Remove the underlying UI object from the hierarchy */
|
175
176
|
protected removeShadowComponent() {
|
176
177
|
if (this.shadowComponent) {
|
177
178
|
this.shadowComponent.removeFromParent();
|
@@ -1,18 +1,18 @@
|
|
1
1
|
import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
|
2
2
|
|
3
|
-
import { isDevEnvironment
|
3
|
+
import { isDevEnvironment } from "../engine/debug/index.js";
|
4
4
|
import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
|
5
5
|
import { activeInHierarchyFieldName } from "../engine/engine_constants.js";
|
6
|
-
import { destroy, findByGuid, foreachComponent, HideFlags, IInstantiateOptions,instantiate,
|
6
|
+
import { destroy, findByGuid, foreachComponent, HideFlags, IInstantiateOptions, instantiate, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
|
7
7
|
import * as main from "../engine/engine_mainloop_utils.js";
|
8
|
-
import { Mathf } from "../engine/engine_math.js";
|
9
8
|
import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
|
10
9
|
import { Context, FrameEvent } from "../engine/engine_setup.js";
|
11
10
|
import * as threeutils from "../engine/engine_three_utils.js";
|
12
|
-
import type { Collision, Constructor, ConstructorConcrete, GuidsMap, ICollider,IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
|
13
|
-
import {
|
11
|
+
import type { Collision, Constructor, ConstructorConcrete, GuidsMap, ICollider, IComponent, IGameObject, SourceIdentifier } from "../engine/engine_types.js";
|
12
|
+
import { INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs } from "../engine/engine_xr.js";
|
14
13
|
import { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
|
15
14
|
|
15
|
+
|
16
16
|
// export interface ISerializationCallbackReceiver {
|
17
17
|
// onBeforeSerialize?(): object | void;
|
18
18
|
// onAfterSerialize?();
|
@@ -125,7 +125,7 @@
|
|
125
125
|
main.addScriptToArrays(comp, context!);
|
126
126
|
if (comp.__internalDidAwakeAndStart) return;
|
127
127
|
if (context!.new_script_start.includes(comp) === false) {
|
128
|
-
context!.new_script_start.push(comp as
|
128
|
+
context!.new_script_start.push(comp as Component);
|
129
129
|
}
|
130
130
|
}, true);
|
131
131
|
}
|
@@ -256,7 +256,7 @@
|
|
256
256
|
return getComponentsInParent(go, typeName, arr);
|
257
257
|
}
|
258
258
|
|
259
|
-
public static getAllComponents(go: IGameObject | Object3D):
|
259
|
+
public static getAllComponents(go: IGameObject | Object3D): Component[] {
|
260
260
|
const componentsList = go.userData?.components;
|
261
261
|
const newList = [...componentsList];
|
262
262
|
return newList;
|
@@ -307,6 +307,12 @@
|
|
307
307
|
|
308
308
|
|
309
309
|
|
310
|
+
/** Needle Engine component base class. Derive from this component to implement your own using the provided lifecycle methods. Components can be added to threejs objects using `GameObject.addComponent`.
|
311
|
+
*
|
312
|
+
* The most common lifecycle methods are `awake`, `start`, `onEanble`, `onDisable` `update` and `onDestroy`.
|
313
|
+
* XR specific callbacks include `onEnterXR`, `onLeaveXR`, `onUpdateXR`, `onControllerAdded` and `onControllerRemoved`.
|
314
|
+
* To receive pointer events implement `onPointerDown`, `onPointerUp`, `onPointerEnter`, `onPointerExit` and `onPointerMove`.
|
315
|
+
*/
|
310
316
|
export abstract class Component implements IComponent, EventTarget,
|
311
317
|
Partial<INeedleXRSessionEventReceiver>,
|
312
318
|
Partial<IPointerEventHandler>
|
@@ -748,7 +754,6 @@
|
|
748
754
|
}
|
749
755
|
}
|
750
756
|
|
751
|
-
|
752
|
-
|
753
|
-
export
|
754
|
-
}
|
757
|
+
// For legacy reasons we need to export this as well
|
758
|
+
// (and we don't use extend to inherit the component docs)
|
759
|
+
export { Component as Behaviour };
|
@@ -31,7 +31,6 @@
|
|
31
31
|
export { BasicIKConstraint } from "../BasicIKConstraint.js";
|
32
32
|
export { BehaviorExtension } from "../export/usdz/extensions/behavior/Behaviour.js";
|
33
33
|
export { BehaviorModel } from "../export/usdz/extensions/behavior/BehavioursBuilder.js";
|
34
|
-
export { Behaviour } from "../Component.js";
|
35
34
|
export { Bloom } from "../postprocessing/Effects/Bloom.js";
|
36
35
|
export { BoxCollider } from "../Collider.js";
|
37
36
|
export { BoxGizmo } from "../Gizmos.js";
|
@@ -35,6 +35,7 @@
|
|
35
35
|
KeyUp = "keyup",
|
36
36
|
KeyPressed = "keypress"
|
37
37
|
}
|
38
|
+
/** e.g. `pointerdown` */
|
38
39
|
export type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
|
39
40
|
|
40
41
|
|
@@ -43,6 +44,8 @@
|
|
43
44
|
{
|
44
45
|
origin: object;
|
45
46
|
pointerId: number;
|
47
|
+
/** the index of the device */
|
48
|
+
deviceIndex: number;
|
46
49
|
pointerType: PointerTypeNames;
|
47
50
|
mode: XRTargetRayMode,
|
48
51
|
ray?: Ray;
|
@@ -55,6 +58,19 @@
|
|
55
58
|
|
56
59
|
export class NEPointerEvent extends PointerEvent {
|
57
60
|
|
61
|
+
/** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
|
62
|
+
override readonly pointerId!: number;
|
63
|
+
|
64
|
+
// this is set via the init arguments (we override it here for intellisense to show the string options)
|
65
|
+
override readonly pointerType!: PointerTypeNames;
|
66
|
+
|
67
|
+
// this is set via the init arguments (we override it here for intellisense to show the string options)
|
68
|
+
/** The input that raised this event like `pointerdown` */
|
69
|
+
override readonly type!: InputEventNames;
|
70
|
+
|
71
|
+
/** the device index: mouse and touch are always 0, otherwise e.g. index of the connected Gamepad or XRController */
|
72
|
+
readonly deviceIndex: number;
|
73
|
+
|
58
74
|
/** The origin of the event contains a reference to the creator of this event.
|
59
75
|
* This can be the Needle Engine input system or e.g. a XR controller
|
60
76
|
*/
|
@@ -77,11 +93,10 @@
|
|
77
93
|
/** true if this event is a double click */
|
78
94
|
isDoubleClick: boolean = false;
|
79
95
|
|
80
|
-
// this is set via the init arguments (we override it here for intellisense to show the string options)
|
81
|
-
override readonly pointerType!: PointerTypeNames;
|
82
96
|
|
83
97
|
constructor(type: InputEvents | InputEventNames, source: Event | null, init: NEPointerEventInit) {
|
84
98
|
super(type, init)
|
99
|
+
this.deviceIndex = init.deviceIndex;
|
85
100
|
this.origin = init.origin;
|
86
101
|
this.source = source;
|
87
102
|
this.mode = init.mode;
|
@@ -359,6 +374,38 @@
|
|
359
374
|
/** This is added/updated for pointers. screenspace pointers set this to the camera near plane */
|
360
375
|
private _pointerSpace: IGameObject[] = [];
|
361
376
|
|
377
|
+
|
378
|
+
|
379
|
+
private readonly _pressedStack = new Map<number, number[]>();
|
380
|
+
private onDownButton(pointerId: number, button: number) {
|
381
|
+
let stack = this._pressedStack.get(pointerId);
|
382
|
+
if (!stack) {
|
383
|
+
stack = [];
|
384
|
+
this._pressedStack.set(pointerId, stack);
|
385
|
+
}
|
386
|
+
stack.push(button);
|
387
|
+
}
|
388
|
+
private onReleaseButton(pointerId: number, button: number) {
|
389
|
+
const stack = this._pressedStack.get(pointerId);
|
390
|
+
if (!stack) return;
|
391
|
+
const index = stack.indexOf(button);
|
392
|
+
if (index >= 0) stack.splice(index, 1);
|
393
|
+
}
|
394
|
+
/** the first button that was down and is currently pressed */
|
395
|
+
getFirstPressedButtonForPointer(pointerId: number): number | undefined {
|
396
|
+
const stack = this._pressedStack.get(pointerId);
|
397
|
+
if (!stack) return undefined;
|
398
|
+
return stack[0];
|
399
|
+
}
|
400
|
+
/** the last (most recent) button that was down and is currently pressed */
|
401
|
+
getLatestPressedButtonForPointer(pointerId: number): number | undefined {
|
402
|
+
const stack = this._pressedStack.get(pointerId);
|
403
|
+
if (!stack) return undefined;
|
404
|
+
return stack[stack.length - 1];
|
405
|
+
}
|
406
|
+
|
407
|
+
|
408
|
+
|
362
409
|
getKeyDown(): string | null {
|
363
410
|
for (const key in this.keysPressed) {
|
364
411
|
const k = this.keysPressed[key];
|
@@ -433,6 +480,7 @@
|
|
433
480
|
switch (args.type) {
|
434
481
|
case InputEvents.PointerDown:
|
435
482
|
if (debug) showBalloonMessage("Create Pointer down");
|
483
|
+
this.onDownButton(args.deviceIndex, args.button);
|
436
484
|
this.onDown(args);
|
437
485
|
break;
|
438
486
|
case InputEvents.PointerMove:
|
@@ -442,6 +490,7 @@
|
|
442
490
|
case InputEvents.PointerUp:
|
443
491
|
if (debug) showBalloonMessage("Create Pointer up");
|
444
492
|
this.onUp(args);
|
493
|
+
this.onReleaseButton(args.deviceIndex, args.button);
|
445
494
|
break;
|
446
495
|
}
|
447
496
|
}
|
@@ -466,11 +515,13 @@
|
|
466
515
|
// e.g. if we have slotted HTML elements in the needle engine DOM elements we don't want to receive input events for those
|
467
516
|
this._htmlEventSource = this.context.renderer.domElement;
|
468
517
|
|
518
|
+
window.addEventListener('contextmenu', this.onContextMenu);
|
469
519
|
|
470
520
|
this._htmlEventSource.addEventListener('touchstart', this.onTouchStart, false);
|
471
521
|
window.addEventListener('touchstart', this.onTouchStartWindow);
|
472
522
|
window.addEventListener('touchmove', this.onTouchMove, { passive: true });
|
473
523
|
window.addEventListener('touchend', this.onTouchUp, false);
|
524
|
+
window.addEventListener("touchcancel", this.onTouchCancel, false);
|
474
525
|
|
475
526
|
this._htmlEventSource.addEventListener('mousedown', this.onMouseDown);
|
476
527
|
window.addEventListener('mousemove', this.onMouseMove, false);
|
@@ -486,10 +537,13 @@
|
|
486
537
|
}
|
487
538
|
|
488
539
|
unbindEvents() {
|
540
|
+
window.removeEventListener('contextmenu', this.onContextMenu);
|
541
|
+
|
489
542
|
this._htmlEventSource?.removeEventListener('touchstart', this.onTouchStart, false);
|
490
543
|
window.removeEventListener('touchstart', this.onTouchStartWindow);
|
491
544
|
window.removeEventListener('touchmove', this.onTouchMove, false);
|
492
545
|
window.removeEventListener('touchend', this.onTouchUp, false);
|
546
|
+
window.removeEventListener("touchcancel", this.onTouchCancel, false);
|
493
547
|
|
494
548
|
this._htmlEventSource?.removeEventListener('mousedown', this.onMouseDown, false);
|
495
549
|
window.removeEventListener('mousemove', this.onMouseMove, false);
|
@@ -548,6 +602,26 @@
|
|
548
602
|
return false;
|
549
603
|
}
|
550
604
|
|
605
|
+
private onContextMenu = (evt: Event) => {
|
606
|
+
if (this.canReceiveInput(evt) === false)
|
607
|
+
return;
|
608
|
+
if (evt instanceof PointerEvent) {
|
609
|
+
// for longpress on touch there might open a context menu
|
610
|
+
// in which case we set the pointer pressed back to false (resetting the pressed pointer)
|
611
|
+
// we need to emit a pointer up event here as well
|
612
|
+
if (evt.pointerType === "touch") {
|
613
|
+
// for (const index in this._pointerPressed) {
|
614
|
+
// if (this._pointerTypes[index] === PointerType.Touch) {
|
615
|
+
// // this._pointerPressed[index] = false;
|
616
|
+
// // this throws orbit controls?
|
617
|
+
// // const ne = this.createPointerEventFromTouch("pointerup", parseInt(index), this._pointerPositions[index].x, this._pointerPositions[index].y, 0, evt);
|
618
|
+
// // this.onUp(ne);
|
619
|
+
// }
|
620
|
+
// }
|
621
|
+
}
|
622
|
+
}
|
623
|
+
}
|
624
|
+
|
551
625
|
private keysPressed: { [key: KeyCode | string]: { pressed: boolean, frame: number, startFrame: number, key: string, code: KeyCode | string } } = {};
|
552
626
|
|
553
627
|
private onKeyDown = (evt: KeyboardEvent) => {
|
@@ -604,7 +678,7 @@
|
|
604
678
|
const id = this.getPointerIndex(touch.identifier)
|
605
679
|
if (debug) showBalloonMessage(`touch start #${id}, identifier:${touch.identifier}`);
|
606
680
|
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
|
607
|
-
const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
|
681
|
+
const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: touch.force });
|
608
682
|
this.onDown(ne);
|
609
683
|
}
|
610
684
|
}
|
@@ -613,9 +687,9 @@
|
|
613
687
|
if (evt.changedTouches.length <= 0) return;
|
614
688
|
for (let i = 0; i < evt.changedTouches.length; i++) {
|
615
689
|
const touch = evt.changedTouches[i];
|
616
|
-
const id = this.getPointerIndex(touch.identifier)
|
690
|
+
const id = this.getPointerIndex(touch.identifier);
|
617
691
|
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
|
618
|
-
const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
|
692
|
+
const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: touch.force });
|
619
693
|
this.onMove(ne);
|
620
694
|
}
|
621
695
|
}
|
@@ -625,55 +699,69 @@
|
|
625
699
|
for (let i = 0; i < evt.changedTouches.length; i++) {
|
626
700
|
const touch = evt.changedTouches[i];
|
627
701
|
const id = this.getPointerIndex(touch.identifier);
|
628
|
-
|
629
702
|
if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) continue;
|
630
|
-
|
631
|
-
if (debug) showBalloonMessage(`touch up #${id}, identifier:${touch.identifier}`);
|
632
|
-
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
|
633
|
-
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
|
703
|
+
const ne = this.createPointerEventFromTouch("pointerup", touch.identifier, touch.clientX, touch.clientY, touch.force, evt);
|
634
704
|
this.onUp(ne);
|
635
705
|
}
|
636
706
|
}
|
707
|
+
private createPointerEventFromTouch(type: InputEventNames, touchIdentifier: number, x: number, y: number, force: number, evt: Event): NEPointerEvent {
|
708
|
+
const id = this.getPointerIndex(touchIdentifier);
|
709
|
+
if (debug) showBalloonMessage(`touch up #${id}, identifier:${touchIdentifier}`);
|
710
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, x, y);
|
711
|
+
const ne = new NEPointerEvent(type, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: x, clientY: y, pointerType: PointerType.Touch, buttonName: "unknown", device: space, pressure: force });
|
712
|
+
return ne;
|
713
|
+
}
|
637
714
|
|
715
|
+
private onTouchCancel = (_evt: Event) => {
|
716
|
+
};
|
717
|
+
|
638
718
|
private onMouseDown = (evt: MouseEvent) => {
|
719
|
+
this.onDownButton(0, evt.button);
|
639
720
|
if (this.context.isInVR) return;
|
640
721
|
if (evt.defaultPrevented) return;
|
641
722
|
if (this.canReceiveInput(evt) === false) return;
|
642
723
|
// TODO: if we have multiple mouse devices we need to get the deviceId
|
643
|
-
const
|
724
|
+
const button = evt.button;
|
644
725
|
let buttonName: MouseButtonName | "none" = "none";
|
645
|
-
switch (
|
726
|
+
switch (button) {
|
646
727
|
case 0: buttonName = "left"; break;
|
647
728
|
case 1: buttonName = "middle"; break;
|
648
729
|
case 2: buttonName = "right"; break;
|
649
730
|
}
|
650
|
-
const
|
651
|
-
const
|
731
|
+
const pointerId = 0 + button;
|
732
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
|
733
|
+
const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, pressure: 1 });
|
652
734
|
this.onDown(ne);
|
653
735
|
}
|
654
736
|
|
655
737
|
private onMouseMove = (evt: MouseEvent) => {
|
656
738
|
if (this.context.isInVR) return;
|
657
739
|
if (evt.defaultPrevented) return;
|
658
|
-
|
659
|
-
const
|
660
|
-
const
|
740
|
+
// take the last pressed button (or should the first pressed button have priority?)
|
741
|
+
const pressedButton = this.getFirstPressedButtonForPointer(0);
|
742
|
+
const button = pressedButton ?? 0;
|
743
|
+
const pointerId = 0 + button;
|
744
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
|
745
|
+
const pressure = pressedButton !== undefined ? 1 : 0;
|
746
|
+
const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: "none", device: space, pressure });
|
661
747
|
this.onMove(ne);
|
662
748
|
}
|
663
749
|
|
664
750
|
private onMouseUp = (evt: MouseEvent) => {
|
751
|
+
this.onReleaseButton(0, evt.button);
|
665
752
|
if (this.context.isInVR) return;
|
666
|
-
|
667
|
-
|
668
|
-
if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) return;
|
753
|
+
const button = evt.button;
|
754
|
+
if (!this.isNewEvent(evt.timeStamp, button, this._pointerUpTimestamp)) return;
|
669
755
|
let buttonName: MouseButtonName | "none" = "none";
|
670
|
-
switch (
|
756
|
+
switch (button) {
|
671
757
|
case 0: buttonName = "left"; break;
|
672
758
|
case 1: buttonName = "middle"; break;
|
673
759
|
case 2: buttonName = "right"; break;
|
674
760
|
}
|
675
|
-
const
|
676
|
-
|
761
|
+
const pointerId = 0 + button;
|
762
|
+
if (evt.defaultPrevented) return;
|
763
|
+
const space = this.getAndUpdateSpatialObjectForScreenPosition(pointerId, evt.clientX, evt.clientY);
|
764
|
+
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, pressure: 0 });
|
677
765
|
this.onUp(ne);
|
678
766
|
}
|
679
767
|
|
@@ -723,8 +811,7 @@
|
|
723
811
|
private onDown(evt: NEPointerEvent) {
|
724
812
|
const index = evt.pointerId;
|
725
813
|
if (this.getPointerPressed(index)) {
|
726
|
-
console.
|
727
|
-
return;
|
814
|
+
console.warn(`pointerId is already pressed: ${index}`, debug ? evt : '');
|
728
815
|
}
|
729
816
|
if (debug) console.log(evt.pointerType, "DOWN", index);
|
730
817
|
if (!this.isInRect(evt)) return;
|
@@ -191,7 +191,7 @@
|
|
191
191
|
}
|
192
192
|
}
|
193
193
|
|
194
|
-
public raycastFromRay(ray: Ray, options: IRaycastOptions |
|
194
|
+
public raycastFromRay(ray: Ray, options: IRaycastOptions | null = null): Array<Intersection> {
|
195
195
|
const opts = options ?? this.defaultRaycastOptions;
|
196
196
|
opts.ray = ray;
|
197
197
|
const res = this.raycast(opts);
|
@@ -206,7 +206,7 @@
|
|
206
206
|
* Use raycastPhysics for raycasting against physic colliders only. Depending on your scenario this might be faster.
|
207
207
|
* @param options raycast options. If null, default options will be used.
|
208
208
|
*/
|
209
|
-
public raycast(options: IRaycastOptions |
|
209
|
+
public raycast(options: IRaycastOptions | null = null): Array<Intersection> {
|
210
210
|
if (!options) options = this.defaultRaycastOptions;
|
211
211
|
const mp = options.screenPoint ?? this.context.input.mousePositionRC;
|
212
212
|
const rc = options.raycaster ?? this.raycaster;
|
@@ -274,8 +274,10 @@
|
|
274
274
|
|
275
275
|
private intersect(raycaster: Raycaster, objects: Object3D[], results: Intersection[], options: IRaycastOptions) {
|
276
276
|
for (const obj of objects) {
|
277
|
+
// dont raycast invisible objects
|
278
|
+
if (obj.visible === false) continue;
|
279
|
+
|
277
280
|
if (Gizmos.isGizmo(obj)) continue;
|
278
|
-
|
279
281
|
// dont raycast object if it's a line and the line threshold is < 0
|
280
282
|
if (options.lineThreshold !== undefined && options.lineThreshold < 0) {
|
281
283
|
if (obj instanceof Line) {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Intersection, Object3D } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
|
4
|
-
import { InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
|
4
|
+
import { Input, InputEventNames, InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
|
5
5
|
import { Mathf } from "../../engine/engine_math.js";
|
6
6
|
import { RaycastOptions, RaycastTestObjectReturnType } from "../../engine/engine_physics.js";
|
7
7
|
import { Context } from "../../engine/engine_setup.js";
|
@@ -10,7 +10,7 @@
|
|
10
10
|
import { Behaviour, GameObject } from "../Component.js";
|
11
11
|
import { $shadowDomOwner } from "./BaseUIComponent.js";
|
12
12
|
import type { ICanvasGroup } from "./Interfaces.js";
|
13
|
-
import { hasPointerEventComponent, type IPointerEventHandler, IPointerUpHandler,PointerEventData } from "./PointerEvents.js";
|
13
|
+
import { hasPointerEventComponent, type IPointerEventHandler, IPointerUpHandler, PointerEventData } from "./PointerEvents.js";
|
14
14
|
import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
|
15
15
|
import { UIRaycastUtils } from "./RaycastUtils.js";
|
16
16
|
import { isUIObject } from "./Utils.js";
|
@@ -92,10 +92,9 @@
|
|
92
92
|
const res = GameObject.findObjectOfType(Raycaster, this.context);
|
93
93
|
if (!res) {
|
94
94
|
const rc = GameObject.addNewComponent(this.context.scene, ObjectRaycaster);
|
95
|
-
rc.ignoreSkinnedMeshes = true;
|
96
95
|
this.raycaster.push(rc);
|
97
96
|
if (isDevEnvironment() || debug)
|
98
|
-
console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.
|
97
|
+
console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.");
|
99
98
|
}
|
100
99
|
}
|
101
100
|
}
|
@@ -153,17 +152,22 @@
|
|
153
152
|
if (pointerEvent.propagationStopped) return;
|
154
153
|
|
155
154
|
// Use the pointerID, this is the touch id or the mouse (always 0, could be index of the mouse DEVICE) or the index of the XRInputSource
|
156
|
-
const
|
157
|
-
|
155
|
+
const data = new PointerEventData(this.context.input, pointerEvent);
|
156
|
+
this._currentPointerEventName = pointerEvent.type;
|
158
157
|
|
159
158
|
data.inputSource = this.context.input;
|
160
|
-
data.
|
159
|
+
data.isClick = pointerEvent.isClick;
|
160
|
+
data.isDoubleClick = pointerEvent.isDoubleClick;
|
161
161
|
// using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
|
162
162
|
data.isDown = pointerEvent.type == InputEvents.PointerDown;
|
163
163
|
data.isUp = pointerEvent.type == InputEvents.PointerUp;
|
164
164
|
data.isPressed = this.context.input.getPointerPressed(pointerEvent.pointerId);
|
165
165
|
|
166
|
-
if (debug
|
166
|
+
if (debug) {
|
167
|
+
if (data.isDown) console.log("DOWN", data.pointerId);
|
168
|
+
else if (data.isUp) console.log("UP", data.pointerId);
|
169
|
+
if (data.isClick) console.log("CLICK", data.pointerId);
|
170
|
+
}
|
167
171
|
|
168
172
|
// raycast
|
169
173
|
const options = new RaycastOptions();
|
@@ -171,13 +175,13 @@
|
|
171
175
|
options.ray = pointerEvent.ray;
|
172
176
|
}
|
173
177
|
else {
|
174
|
-
options.screenPoint = this.context.input.getPointerPositionRC(
|
178
|
+
options.screenPoint = this.context.input.getPointerPositionRC(pointerEvent.pointerId)!;
|
175
179
|
}
|
176
180
|
|
177
181
|
|
178
182
|
const hits = this.performRaycast(options);
|
179
183
|
|
180
|
-
if (debug && data.
|
184
|
+
if (debug && data.isClick) {
|
181
185
|
showBalloonMessage("EventSystem: " + data.pointerId + " - " + this.context.time.frame + " - Up:" + data.isUp + ", Down:" + data.isDown)
|
182
186
|
}
|
183
187
|
|
@@ -190,7 +194,7 @@
|
|
190
194
|
this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }));
|
191
195
|
|
192
196
|
// then handle the intersections and call the callbacks on the regular objects
|
193
|
-
this.handleIntersections(
|
197
|
+
this.handleIntersections(hits, data);
|
194
198
|
|
195
199
|
this.dispatchEvent(new CustomEvent<AfterHandleInputEvent>(EventSystemEvents.AfterHandleInput, { detail: evt }));
|
196
200
|
}
|
@@ -203,6 +207,7 @@
|
|
203
207
|
private readonly _testObjectsCache = new Map<Object3D, boolean>();
|
204
208
|
/** that's the raycaster that is CURRENTLY being used for raycasting (the shouldRaycastObject method uses this) */
|
205
209
|
private _currentlyActiveRaycaster: Raycaster | null = null;
|
210
|
+
private _currentPointerEventName: InputEventNames | null = null;
|
206
211
|
|
207
212
|
/**
|
208
213
|
* Checks if an object that we encounter has an event component and if it does, we add it to our objects cache
|
@@ -239,8 +244,8 @@
|
|
239
244
|
// if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return false;
|
240
245
|
|
241
246
|
// the object was not yet seen so we test if it has an event component
|
242
|
-
let hasEventComponent = hasPointerEventComponent(obj);
|
243
|
-
if (!hasEventComponent && uiOwner) hasEventComponent = hasPointerEventComponent(uiOwner);
|
247
|
+
let hasEventComponent = hasPointerEventComponent(obj, this._currentPointerEventName);
|
248
|
+
if (!hasEventComponent && uiOwner) hasEventComponent = hasPointerEventComponent(uiOwner, this._currentPointerEventName);
|
244
249
|
|
245
250
|
if (hasEventComponent) {
|
246
251
|
// it has an event component: we add it and all its children to the cache
|
@@ -311,13 +316,8 @@
|
|
311
316
|
}
|
312
317
|
}
|
313
318
|
|
314
|
-
private handleIntersections(
|
319
|
+
private handleIntersections(hits: Intersection[] | null | undefined, args: PointerEventData): boolean {
|
315
320
|
|
316
|
-
// first invoke captured pointers
|
317
|
-
this.assignHitInformation(args, hits?.[0]);
|
318
|
-
this.invokePointerCapture(args);
|
319
|
-
|
320
|
-
|
321
321
|
if (hits?.length) {
|
322
322
|
hits = this.sortCandidates(hits);
|
323
323
|
for (const hit of hits) {
|
@@ -331,20 +331,23 @@
|
|
331
331
|
}
|
332
332
|
}
|
333
333
|
|
334
|
+
// first invoke captured pointers
|
335
|
+
this.assignHitInformation(args, hits?.[0]);
|
336
|
+
this.invokePointerCapture(args);
|
337
|
+
|
334
338
|
// pointer has not hit any object to handle
|
335
339
|
|
336
340
|
// thus is not hovering over anything
|
337
|
-
const hoveredData = this.hoveredByID.get(
|
341
|
+
const hoveredData = this.hoveredByID.get(args.pointerId);
|
338
342
|
if (hoveredData) {
|
339
343
|
this.triggerOnExit(hoveredData.obj, hoveredData.data, null);
|
340
344
|
}
|
341
|
-
this.hoveredByID.delete(
|
345
|
+
this.hoveredByID.delete(args.pointerId);
|
342
346
|
|
343
347
|
// if it was up, it means it doesn't should notify things that it down on before
|
344
348
|
if (args.isUp) {
|
345
|
-
|
346
|
-
|
347
|
-
this.pressedByID.delete(id);
|
349
|
+
this.pressedByID.get(args.pointerId)?.handlers.forEach(h => this.invokeOnPointerUp(args, h));
|
350
|
+
this.pressedByID.delete(args.pointerId);
|
348
351
|
}
|
349
352
|
|
350
353
|
return false;
|
@@ -385,7 +388,7 @@
|
|
385
388
|
private handleEventOnObject(object: THREE.Object3D, args: PointerEventData): boolean {
|
386
389
|
// ensures that invisible objects are ignored
|
387
390
|
if (!this.testIsVisible(object)) {
|
388
|
-
if (args.
|
391
|
+
if (args.isClick && debug)
|
389
392
|
console.log("not allowed", object);
|
390
393
|
return false;
|
391
394
|
}
|
@@ -401,13 +404,13 @@
|
|
401
404
|
|
402
405
|
const parent = object.parent as any;
|
403
406
|
let isShadow = false;
|
404
|
-
const clicked = args.
|
407
|
+
const clicked = args.isClick ?? false;
|
405
408
|
|
406
409
|
let canvasGroup: ICanvasGroup | null = null;
|
407
410
|
|
408
411
|
// handle potential shadow dom built from three mesh ui
|
409
412
|
if (parent && parent.isUI) {
|
410
|
-
const pressedOrClicked = (args.isPressed || args.
|
413
|
+
const pressedOrClicked = (args.isPressed || args.isClick) ?? false;
|
411
414
|
if (parent[$shadowDomOwner]) {
|
412
415
|
const actualGo = parent[$shadowDomOwner].gameObject;
|
413
416
|
if (actualGo) {
|
@@ -494,7 +497,6 @@
|
|
494
497
|
* Propagate up in hiearchy and call handlers based on the pointer event data
|
495
498
|
*/
|
496
499
|
private handleMainInteraction(object: Object3D, args: PointerEventData, prevHovering: Object3D | null) {
|
497
|
-
if (args.pointerId === undefined) return;
|
498
500
|
const pressedEvent = this.pressedByID.get(args.pointerId);
|
499
501
|
const hoveredObjectChanged = prevHovering !== object;
|
500
502
|
|
@@ -546,7 +548,7 @@
|
|
546
548
|
if (comp.onPointerMove) {
|
547
549
|
if (isMoving)
|
548
550
|
comp.onPointerMove(args);
|
549
|
-
|
551
|
+
this.handlePointerCapture(args, comp);
|
550
552
|
}
|
551
553
|
|
552
554
|
if (args.isUp) {
|
@@ -559,16 +561,9 @@
|
|
559
561
|
// The original component that received the down event SHOULD also receive the up event
|
560
562
|
pressedEvent?.handlers.delete(comp);
|
561
563
|
}
|
562
|
-
|
563
|
-
// handle onExit on touchUp
|
564
|
-
// onExit on mouse is handled when we hover over something else / on nothing
|
565
|
-
if (comp.onPointerExit && args.event?.pointerType === PointerType.Touch) {
|
566
|
-
comp.onPointerExit(args);
|
567
|
-
this.hoveredByID.delete(args.pointerId!);
|
568
|
-
}
|
569
564
|
}
|
570
565
|
|
571
|
-
if (args.
|
566
|
+
if (args.isClick) {
|
572
567
|
if (comp.onPointerClick) {
|
573
568
|
comp.onPointerClick(args);
|
574
569
|
}
|
@@ -611,48 +606,54 @@
|
|
611
606
|
/** handles onPointerUp - this will also release the pointerCapture */
|
612
607
|
private invokeOnPointerUp(evt: PointerEventData, handler: IPointerUpHandler) {
|
613
608
|
handler.onPointerUp?.call(handler, evt);
|
614
|
-
this.releasePointerCapture(evt
|
609
|
+
this.releasePointerCapture(evt, handler);
|
615
610
|
}
|
616
611
|
|
617
612
|
/** the list of component handlers that requested pointerCapture for a specific pointerId */
|
618
|
-
private readonly _capturedPointer: { [
|
613
|
+
private readonly _capturedPointer: { [id: number]: IPointerEventHandler[] } = {};
|
619
614
|
|
620
615
|
/** check if the event was marked to be captured: if yes add the current component to the captured list */
|
621
616
|
private handlePointerCapture(evt: PointerEventData, comp: IPointerEventHandler) {
|
622
617
|
if (evt.z__pointer_ctured) {
|
623
618
|
evt.z__pointer_ctured = false;
|
619
|
+
const id = evt.pointerId;
|
624
620
|
// only the onPointerMove event is called with captured pointers so we don't need to add it to our list if it doesnt implement onPointerMove
|
625
621
|
if (comp.onPointerMove) {
|
626
|
-
const list = this._capturedPointer[
|
622
|
+
const list = this._capturedPointer[id] || [];
|
627
623
|
list.push(comp);
|
628
|
-
this._capturedPointer[
|
624
|
+
this._capturedPointer[id] = list;
|
629
625
|
}
|
630
626
|
else {
|
631
|
-
if (isDevEnvironment())
|
627
|
+
if (isDevEnvironment() && !comp["z__warned_no_pointermove"]) {
|
628
|
+
comp["z__warned_no_pointermove"] = true;
|
632
629
|
console.warn("PointerCapture was requested but the component doesn't implement onPointerMove. It will not receive any pointer events");
|
630
|
+
}
|
633
631
|
}
|
634
632
|
}
|
635
633
|
else if (evt.z__pointer_cture_rleased) {
|
636
634
|
evt.z__pointer_cture_rleased = false;
|
637
|
-
this.releasePointerCapture(evt
|
635
|
+
this.releasePointerCapture(evt, comp);
|
638
636
|
}
|
639
637
|
}
|
640
638
|
|
641
639
|
/** removes the component from the pointer capture list */
|
642
|
-
releasePointerCapture(
|
643
|
-
|
644
|
-
|
640
|
+
releasePointerCapture(evt: PointerEventData, component: IPointerEventHandler) {
|
641
|
+
const id = evt.pointerId;
|
642
|
+
if (this._capturedPointer[id]) {
|
643
|
+
const i = this._capturedPointer[id].indexOf(component);
|
645
644
|
if (i !== -1) {
|
646
|
-
this._capturedPointer[
|
645
|
+
this._capturedPointer[id].splice(i, 1);
|
646
|
+
if (debug) console.log("released pointer capture", id, component, this._capturedPointer)
|
647
647
|
}
|
648
648
|
}
|
649
649
|
}
|
650
650
|
/** invoke the pointerMove event on all captured handlers */
|
651
651
|
private invokePointerCapture(evt: PointerEventData) {
|
652
652
|
if (evt.event.type === InputEvents.PointerMove) {
|
653
|
-
const
|
654
|
-
const captured = this._capturedPointer[
|
653
|
+
const id = evt.pointerId;
|
654
|
+
const captured = this._capturedPointer[id];
|
655
655
|
if (captured) {
|
656
|
+
if (debug) console.log("Captured", id, captured)
|
656
657
|
for (let i = 0; i < captured.length; i++) {
|
657
658
|
const handler = captured[i];
|
658
659
|
// check if it was destroyed
|
@@ -1,9 +1,9 @@
|
|
1
|
-
import { fetchProfile,MotionController } from "@webxr-input-profiles/motion-controllers";
|
1
|
+
import { fetchProfile, MotionController } from "@webxr-input-profiles/motion-controllers";
|
2
2
|
import { AxesHelper, Int8BufferAttribute, Object3D, Quaternion, Ray, Vector3 } from "three";
|
3
3
|
|
4
4
|
import { Context } from "../engine_context.js";
|
5
5
|
import { Gizmos } from "../engine_gizmos.js";
|
6
|
-
import { InputEventNames,InputEvents, NEPointerEvent, NEPointerEventInit, PointerType } from "../engine_input.js";
|
6
|
+
import { InputEventNames, InputEvents, NEPointerEvent, NEPointerEventInit, PointerType } from "../engine_input.js";
|
7
7
|
import { getTempQuaternion, getTempVector, getWorldQuaternion } from "../engine_three_utils.js";
|
8
8
|
import type { ButtonName, IGameObject, Vec3, XRControllerButtonName } from "../engine_types.js";
|
9
9
|
import { getParam } from "../engine_utils.js";
|
@@ -207,6 +207,7 @@
|
|
207
207
|
this.pointerInit = {
|
208
208
|
origin: this,
|
209
209
|
pointerType: this.hand ? "hand" : "controller",
|
210
|
+
deviceIndex: this.index,
|
210
211
|
pointerId: -1, // < this will be updated in the emitPointerEvent method
|
211
212
|
mode: this.inputSource.targetRayMode,
|
212
213
|
ray: this._ray,
|
@@ -519,7 +520,7 @@
|
|
519
520
|
// and start with index = 1
|
520
521
|
private updateInputEvents() {
|
521
522
|
if (!this._layout) return;
|
522
|
-
|
523
|
+
|
523
524
|
// https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-heading
|
524
525
|
if (this.gamepad?.buttons) {
|
525
526
|
for (let k = 0; k < this.gamepad.buttons.length; k++) {
|
@@ -553,22 +554,25 @@
|
|
553
554
|
|
554
555
|
if (eventName != null && emitEvent) {
|
555
556
|
const name = this._layout?.gamepad[k];
|
556
|
-
this.emitPointerEvent(eventName, k, name ?? "none", false);
|
557
|
+
this.emitPointerEvent(eventName, k, name ?? "none", false, null, button.value);
|
557
558
|
}
|
558
559
|
}
|
559
560
|
}
|
560
561
|
}
|
561
562
|
private onUpdateMove() {
|
562
|
-
this.
|
563
|
+
let button = this.xr.context.input.getFirstPressedButtonForPointer(this.index);
|
564
|
+
if (button === undefined) button = 0;
|
565
|
+
const pressure = this.gamepad?.buttons[button]?.value;
|
566
|
+
this.emitPointerEvent("pointermove", button, "none", false, null, pressure);
|
563
567
|
}
|
564
568
|
|
565
569
|
|
566
570
|
/** cached spatial pointer init object. We re-use it to not have */
|
567
571
|
private readonly pointerInit: NEPointerEventInit;
|
568
|
-
private emitPointerEvent(type: InputEventNames, button: number, buttonName: ButtonName | "none", primary: boolean, source: Event | null = null) {
|
572
|
+
private emitPointerEvent(type: InputEventNames, button: number, buttonName: ButtonName | "none", primary: boolean, source: Event | null = null, pressure?: number) {
|
569
573
|
|
570
574
|
if (!this.emitEvents) {
|
571
|
-
if(debug && type !== InputEvents.PointerMove) console.warn("Pointer events are disabled for this controller", this.index, type, button);
|
575
|
+
if (debug && type !== InputEvents.PointerMove) console.warn("Pointer events are disabled for this controller", this.index, type, button);
|
572
576
|
return;
|
573
577
|
}
|
574
578
|
|
@@ -577,7 +581,6 @@
|
|
577
581
|
// Not sure if *this* is enough to determine if the event is spatial or not
|
578
582
|
if (this.xr.mode === "immersive-vr" || this.xr.isPassThrough) {
|
579
583
|
this.pointerInit.origin = this;
|
580
|
-
// TODO: this needs to be just the index (pointerId)
|
581
584
|
this.pointerInit.pointerId = this.index * 10 + button;
|
582
585
|
this.pointerInit.pointerType = this.hand ? "hand" : "controller";
|
583
586
|
this.pointerInit.button = button;
|
@@ -586,6 +589,7 @@
|
|
586
589
|
this.pointerInit.mode = this.inputSource.targetRayMode;
|
587
590
|
this.pointerInit.ray = this.ray;
|
588
591
|
this.pointerInit.device = this.object;
|
592
|
+
this.pointerInit.pressure = pressure;
|
589
593
|
|
590
594
|
const prevContext = Context.Current;
|
591
595
|
Context.Current = this.xr.context;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Camera, DoubleSide,Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, PlaneGeometry, PlaneHelper, Quaternion, Vector3, WebXRArrayCamera } from "three";
|
1
|
+
import { Camera, DoubleSide, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, PlaneGeometry, PlaneHelper, Quaternion, Vector3, WebXRArrayCamera } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
|
4
4
|
import { Context, FrameEvent } from "../engine_context.js";
|
@@ -10,13 +10,15 @@
|
|
10
10
|
import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
|
11
11
|
import { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
|
12
12
|
import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
|
13
|
-
import { flipForwardMatrix, flipForwardQuaternion,ImplictXRRig } from "./internal.js";
|
13
|
+
import { flipForwardMatrix, flipForwardQuaternion, ImplictXRRig } from "./internal.js";
|
14
14
|
import { NeedleXRController } from "./NeedleXRController.js";
|
15
15
|
import { NeedleXRSync } from "./NeedleXRSync.js";
|
16
16
|
import { SceneTransition } from "./SceneTransition.js";
|
17
17
|
import { TemporaryXRContext } from "./TempXRContext.js";
|
18
18
|
import type { IXRRig } from "./XRRig.js";
|
19
19
|
|
20
|
+
/** NeedleXRSession event argument.
|
21
|
+
* Use `args.xr` to access the NeedleXRSession */
|
20
22
|
export type NeedleXREventArgs = { xr: NeedleXRSession }
|
21
23
|
export type SessionChangedEvt = (args: NeedleXREventArgs) => void;
|
22
24
|
export type SessionRequestedEvent = (args: { mode: XRSessionMode, init: XRSessionInit }) => void;
|
@@ -46,7 +48,9 @@
|
|
46
48
|
|
47
49
|
/** Contains a reference to the currently active webxr session and the controller that has changed */
|
48
50
|
export type NeedleXRControllerEventArgs = NeedleXREventArgs & { controller: NeedleXRController, change: "added" | "removed" }
|
49
|
-
/** Event Arguments when a controller changed event is invoked (added or removed)
|
51
|
+
/** Event Arguments when a controller changed event is invoked (added or removed)
|
52
|
+
* Access the controller via `args.controller`, the `args.change` property indicates if the controller was added or removed
|
53
|
+
*/
|
50
54
|
export type ControllerChangedEvt = (args: NeedleXRControllerEventArgs) => void;
|
51
55
|
|
52
56
|
|
@@ -128,17 +132,28 @@
|
|
128
132
|
}
|
129
133
|
|
130
134
|
/**
|
131
|
-
* This class manages an XRSession to provide helper methods and events
|
132
|
-
* It provides easy access to the XRInputSources (controllers and hands)
|
133
|
-
* If a XRSession is active you can use all XR-related event methods on your components to receive XR events
|
135
|
+
* This class manages an XRSession to provide helper methods and events. It provides easy access to the XRInputSources (controllers and hands)
|
134
136
|
* - Start a XRSession with `NeedleXRSession.start(...)`
|
135
137
|
* - Stop a XRSession with `NeedleXRSession.stop()`
|
136
|
-
* - Access running XRSession with `NeedleXRSession.active`
|
137
|
-
* - Listen to XRSession start events with `NeedleXRSession.onXRStart(...)`
|
138
|
-
* - Listen to XRSession end events with `NeedleXRSession.onXREnd(...)`
|
139
|
-
* - Listen to XRSession controller added events with `NeedleXRSession.onControllerAdded(...)`
|
140
|
-
* - Listen to XRSession controller removed events with `NeedleXRSession.onControllerRemoved(...)`
|
138
|
+
* - Access a running XRSession with `NeedleXRSession.active`
|
141
139
|
*
|
140
|
+
* If a XRSession is active you can use all XR-related event methods on your components to receive XR events e.g. `onEnterXR`, `onUpdateXR`, `onLeaveXR`
|
141
|
+
* ```ts
|
142
|
+
* export class MyComponent extends Behaviour {
|
143
|
+
* // callback invoked whenever the XRSession is started or your component is added to a scene with an active XRSession
|
144
|
+
* onEnterXR(args: NeedleXREventArgs) {
|
145
|
+
* console.log("Entered XR");
|
146
|
+
* // access the NeedleXRSession via args.xr
|
147
|
+
* }
|
148
|
+
* // callback invoked whenever a controller is added (or you switch from controller to hand tracking)
|
149
|
+
* onControllerAdded(args: NeedleXRControllerEventArgs) { }
|
150
|
+
* }
|
151
|
+
* ```
|
152
|
+
*
|
153
|
+
* ### XRRig
|
154
|
+
* The XRRig can be accessed via the `rig` property
|
155
|
+
* Set a custom XRRig via `NeedleXRSession.addRig(...)` or `NeedleXRSession.removeRig(...)`
|
156
|
+
* By default the active XRRig with the highest priority in the scene is used
|
142
157
|
*/
|
143
158
|
export class NeedleXRSession implements INeedleXRSession {
|
144
159
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { Face, Object3D, Vector3 } from "three";
|
2
2
|
|
3
|
-
import { Input, NEPointerEvent } from "../../engine/engine_input.js";
|
3
|
+
import { Input, InputEventNames, NEPointerEvent } from "../../engine/engine_input.js";
|
4
4
|
import { GamepadButtonName, MouseButtonName } from "../../engine/engine_types.js";
|
5
5
|
import { GameObject } from "../Component.js";
|
6
6
|
|
@@ -19,13 +19,21 @@
|
|
19
19
|
|
20
20
|
/** the original event */
|
21
21
|
readonly event: NEPointerEvent;
|
22
|
-
|
23
|
-
|
22
|
+
|
23
|
+
/** the index of the used device
|
24
|
+
* mouse and touch are always 0, controller is the gamepad index or XRController index
|
25
|
+
*/
|
26
|
+
get deviceIndex() { return this.event.deviceIndex; }
|
27
|
+
|
28
|
+
/** a combination of the pointerId + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
|
29
|
+
get pointerId() { return this.event.pointerId; }
|
30
|
+
|
24
31
|
/**
|
25
32
|
* mouse button 0 === LEFT, 1 === MIDDLE, 2 === RIGHT
|
26
33
|
* */
|
27
34
|
readonly button: number;
|
28
35
|
readonly buttonName: MouseButtonName | GamepadButtonName | undefined;
|
36
|
+
get pressure(): number { return this.event.pressure; }
|
29
37
|
|
30
38
|
private _used: boolean = false;
|
31
39
|
/** true when `use()` has been called */
|
@@ -98,20 +106,20 @@
|
|
98
106
|
isDown: boolean | undefined;
|
99
107
|
isUp: boolean | undefined;
|
100
108
|
isPressed: boolean | undefined;
|
101
|
-
|
109
|
+
isClick: boolean | undefined;
|
110
|
+
isDoubleClick: boolean | undefined;
|
102
111
|
|
103
112
|
|
104
113
|
private input: Input;
|
105
114
|
|
106
|
-
constructor(
|
107
|
-
this.pointerId = pointerId;
|
115
|
+
constructor(input: Input, event: NEPointerEvent) {
|
108
116
|
this.event = event;
|
109
117
|
this.input = input;
|
110
118
|
this.button = event.button;
|
111
119
|
}
|
112
120
|
|
113
121
|
clone() {
|
114
|
-
const clone = new PointerEventData(this.
|
122
|
+
const clone = new PointerEventData(this.input, this.event);
|
115
123
|
Object.assign(clone, this);
|
116
124
|
return clone;
|
117
125
|
}
|
@@ -167,11 +175,27 @@
|
|
167
175
|
* @internal tests if the object has any PointerEventComponent used by the EventSystem
|
168
176
|
* This is used to skip raycasting on objects that have no components that use pointer events
|
169
177
|
*/
|
170
|
-
export function hasPointerEventComponent(obj: Object3D) {
|
178
|
+
export function hasPointerEventComponent(obj: Object3D, event?: InputEventNames | null) {
|
171
179
|
const res = GameObject.foreachComponent(obj, comp => {
|
172
180
|
const handler = comp as IPointerEventHandler;
|
173
|
-
if
|
174
|
-
|
181
|
+
// if a specific event is passed in, we only check for that event
|
182
|
+
if (event) {
|
183
|
+
switch (event) {
|
184
|
+
case "pointerdown":
|
185
|
+
if (handler.onPointerDown) return true;
|
186
|
+
break;
|
187
|
+
case "pointerup":
|
188
|
+
if (handler.onPointerUp || handler.onPointerClick) return true;
|
189
|
+
break;
|
190
|
+
case "pointermove":
|
191
|
+
if (handler.onPointerEnter || handler.onPointerExit || handler.onPointerMove) return true;
|
192
|
+
break;
|
193
|
+
}
|
194
|
+
}
|
195
|
+
else {
|
196
|
+
if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
|
197
|
+
return true;
|
198
|
+
}
|
175
199
|
// undefined means continue
|
176
200
|
return undefined;
|
177
201
|
}, false);
|
@@ -33,7 +33,6 @@
|
|
33
33
|
import { BasicIKConstraint } from "../../engine-components/BasicIKConstraint.js";
|
34
34
|
import { BehaviorExtension } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
|
35
35
|
import { BehaviorModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
36
|
-
import { Behaviour } from "../../engine-components/Component.js";
|
37
36
|
import { Bloom } from "../../engine-components/postprocessing/Effects/Bloom.js";
|
38
37
|
import { BoxCollider } from "../../engine-components/Collider.js";
|
39
38
|
import { BoxGizmo } from "../../engine-components/Gizmos.js";
|
@@ -248,7 +247,6 @@
|
|
248
247
|
TypeStore.add("BasicIKConstraint", BasicIKConstraint);
|
249
248
|
TypeStore.add("BehaviorExtension", BehaviorExtension);
|
250
249
|
TypeStore.add("BehaviorModel", BehaviorModel);
|
251
|
-
TypeStore.add("Behaviour", Behaviour);
|
252
250
|
TypeStore.add("Bloom", Bloom);
|
253
251
|
TypeStore.add("BoxCollider", BoxCollider);
|
254
252
|
TypeStore.add("BoxGizmo", BoxGizmo);
|
@@ -5,8 +5,9 @@
|
|
5
5
|
|
6
6
|
import { Gizmos } from "../../../engine/engine_gizmos.js";
|
7
7
|
import { Mathf } from "../../../engine/engine_math.js";
|
8
|
+
import { RaycastTestObjectCallback } from "../../../engine/engine_physics.js";
|
8
9
|
import { serializable } from "../../../engine/engine_serialization.js"
|
9
|
-
import { getWorldQuaternion, getWorldScale } from "../../../engine/engine_three_utils.js";
|
10
|
+
import { getTempVector, getWorldQuaternion, getWorldScale } from "../../../engine/engine_three_utils.js";
|
10
11
|
import { IGameObject } from "../../../engine/engine_types.js";
|
11
12
|
import { getParam } from "../../../engine/engine_utils.js";
|
12
13
|
import { NeedleXRController, NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
|
@@ -160,6 +161,7 @@
|
|
160
161
|
|
161
162
|
private readonly _lines: Object3D[] = [];
|
162
163
|
private readonly _hitDiscs: Object3D[] = [];
|
164
|
+
private readonly _hitDistances: number[] = [];
|
163
165
|
|
164
166
|
protected renderRays(session: NeedleXRSession) {
|
165
167
|
|
@@ -188,7 +190,8 @@
|
|
188
190
|
line.position.copy(pos);
|
189
191
|
line.quaternion.copy(rot);
|
190
192
|
const scale = session.rigScale;
|
191
|
-
|
193
|
+
const dist = this._hitDistances[i] ?? 1;
|
194
|
+
line.scale.set(scale, scale, scale * dist);
|
192
195
|
line.visible = true;
|
193
196
|
line.layers.disableAll();
|
194
197
|
line.layers.enable(2);
|
@@ -203,11 +206,14 @@
|
|
203
206
|
}
|
204
207
|
for (let i = 0; i < session.controllers.length; i++) {
|
205
208
|
const ctrl = session.controllers[i];
|
206
|
-
|
207
|
-
|
209
|
+
const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter })[0];
|
210
|
+
this._hitDistances[i] = hit?.distance;
|
208
211
|
if (hit) {
|
209
212
|
const rigScale = (session.rigScale ?? 1);
|
210
|
-
if (debug)
|
213
|
+
if (debug) {
|
214
|
+
Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000, .2);
|
215
|
+
Gizmos.DrawLabel(getTempVector(0, .2, 0).add(hit.point), hit.object.name, .02, 0);
|
216
|
+
}
|
211
217
|
|
212
218
|
let disc = this._hitDiscs[i];
|
213
219
|
if (!disc) {
|
@@ -243,6 +249,12 @@
|
|
243
249
|
}
|
244
250
|
}
|
245
251
|
|
252
|
+
protected hitPointRaycastFilter: RaycastTestObjectCallback = (obj: Object3D) => {
|
253
|
+
// by default dont raycast ont skinned meshes using the hit point raycast (because it is a big performance hit and only a visual indicator)
|
254
|
+
if (obj.type === "SkinnedMesh") return "continue in children";
|
255
|
+
return true;
|
256
|
+
}
|
257
|
+
|
246
258
|
/** create an object to visualize hit points in the scene */
|
247
259
|
protected createHitPointObject(): Object3D {
|
248
260
|
var container = new Object3D();
|