Needle Engine

Changes between version 3.32.8-alpha and 3.32.9-alpha
Files changed (11) hide show
  1. src/engine-components/ui/BaseUIComponent.ts +22 -21
  2. src/engine-components/Component.ts +16 -11
  3. src/engine-components/codegen/components.ts +0 -1
  4. src/engine/engine_input.ts +112 -25
  5. src/engine/engine_physics.ts +5 -3
  6. src/engine-components/ui/EventSystem.ts +50 -49
  7. src/engine/xr/NeedleXRController.ts +12 -8
  8. src/engine/xr/NeedleXRSession.ts +26 -11
  9. src/engine-components/ui/PointerEvents.ts +34 -10
  10. src/engine/codegen/register_types.ts +0 -2
  11. src/engine-components/webxr/controllers/XRControllerMovement.ts +17 -5
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -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
- shadowComponent: Object3D | null = null;
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
- //@ts-ignore
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();
src/engine-components/Component.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
2
2
 
3
- import { isDevEnvironment,showBalloonWarning } from "../engine/debug/index.js";
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, InstantiateOptions, isActiveInHierarchy, isActiveSelf, isDestroyed, isUsingInstancing, markAsInstancedRendered, setActive } from "../engine/engine_gameobject.js";
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 { ControllerChangedEvt, INeedleXRSessionEventReceiver, NeedleXRControllerEventArgs, NeedleXREventArgs, NeedleXRSession } from "../engine/engine_xr.js";
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 Behaviour);
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): Behaviour[] {
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 class Behaviour extends Component {
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 };
src/engine-components/codegen/components.ts CHANGED
@@ -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";
src/engine/engine_input.ts CHANGED
@@ -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 id = evt.button;
724
+ const button = evt.button;
644
725
  let buttonName: MouseButtonName | "none" = "none";
645
- switch (id) {
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 space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
651
- const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space });
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
- const id = evt.button;
659
- const space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
660
- const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: "none", device: space });
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
- if (evt.defaultPrevented) return;
667
- const id = evt.button;
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 (id) {
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 space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
676
- const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, });
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.error("ERROR: pointerId is already pressed", index);
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;
src/engine/engine_physics.ts CHANGED
@@ -191,7 +191,7 @@
191
191
  }
192
192
  }
193
193
 
194
- public raycastFromRay(ray: Ray, options: IRaycastOptions | RaycastOptions | null = null): Array<Intersection> {
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 | RaycastOptions | null = null): Array<Intersection> {
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) {
src/engine-components/ui/EventSystem.ts CHANGED
@@ -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. Skinnedmeshes will be ignored for better performance");
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 id = pointerEvent.pointerId * 100 + pointerEvent.button;
157
- const data = new PointerEventData(id, this.context.input, pointerEvent);
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.isClicked = pointerEvent.isClick;
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 && data.isClicked) console.log("CLICK", data.pointerId);
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(id)!;
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.isClicked) {
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(id, hits, data);
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(id: number, hits: Intersection[] | null | undefined, args: PointerEventData): boolean {
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(id);
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(id);
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
- const pressedData = this.pressedByID.get(id);
346
- pressedData?.handlers.forEach(h => this.invokeOnPointerUp(args, h));
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.isClicked && debug)
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.isClicked ?? false;
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.isClicked) ?? false;
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
- this.handlePointerCapture(args, comp);
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.isClicked) {
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.pointerId, handler);
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: { [pointerId: number]: IPointerEventHandler[] } = {};
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[evt.pointerId] || [];
622
+ const list = this._capturedPointer[id] || [];
627
623
  list.push(comp);
628
- this._capturedPointer[evt.pointerId] = list;
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.pointerId, comp);
635
+ this.releasePointerCapture(evt, comp);
638
636
  }
639
637
  }
640
638
 
641
639
  /** removes the component from the pointer capture list */
642
- releasePointerCapture(pointerId: number, component: IPointerEventHandler) {
643
- if (this._capturedPointer[pointerId]) {
644
- const i = this._capturedPointer[pointerId].indexOf(component);
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[pointerId].splice(i, 1);
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 pointerId = evt.pointerId;
654
- const captured = this._capturedPointer[pointerId];
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
src/engine/xr/NeedleXRController.ts CHANGED
@@ -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.emitPointerEvent("pointermove", 0, "none", false);
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;
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -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
 
src/engine-components/ui/PointerEvents.ts CHANGED
@@ -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
- /** the pointer identifier for this event */
23
- readonly pointerId: number;
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
- isClicked: boolean | undefined;
109
+ isClick: boolean | undefined;
110
+ isDoubleClick: boolean | undefined;
102
111
 
103
112
 
104
113
  private input: Input;
105
114
 
106
- constructor(pointerId: number, input: Input, event: NEPointerEvent) {
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.pointerId, this.input, this.event);
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 (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
174
- return true;
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);
src/engine/codegen/register_types.ts CHANGED
@@ -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);
src/engine-components/webxr/controllers/XRControllerMovement.ts CHANGED
@@ -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
- line.scale.set(scale, scale, scale);
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
- const hit = this.context.physics.raycastFromRay(ctrl.ray, {})[0];
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) Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000, .2);
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();