InteractiveGroup.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {
  2. Group,
  3. Raycaster,
  4. Vector2
  5. } from 'three';
  6. const _pointer = new Vector2();
  7. const _event = { type: '', data: _pointer };
  8. // The XR events that are mapped to "standard" pointer events.
  9. const _events = {
  10. 'move': 'mousemove',
  11. 'select': 'click',
  12. 'selectstart': 'mousedown',
  13. 'selectend': 'mouseup'
  14. };
  15. const _raycaster = new Raycaster();
  16. /**
  17. * This class can be used to group 3D objects in an interactive group.
  18. * The group itself can listen to Pointer, Mouse or XR controller events to
  19. * detect selections of descendant 3D objects. If a 3D object is selected,
  20. * the respective event is going to dispatched to it.
  21. *
  22. * ```js
  23. * const group = new InteractiveGroup();
  24. * group.listenToPointerEvents( renderer, camera );
  25. * group.listenToXRControllerEvents( controller1 );
  26. * group.listenToXRControllerEvents( controller2 );
  27. * scene.add( group );
  28. *
  29. * // now add objects that should be interactive
  30. * group.add( mesh1, mesh2, mesh3 );
  31. * ```
  32. * @augments Group
  33. * @three_import import { InteractiveGroup } from 'three/addons/interactive/InteractiveGroup.js';
  34. */
  35. class InteractiveGroup extends Group {
  36. constructor() {
  37. super();
  38. /**
  39. * The internal raycaster.
  40. *
  41. * @type {Raycaster}
  42. */
  43. this.raycaster = new Raycaster();
  44. /**
  45. * The internal raycaster.
  46. *
  47. * @type {?HTMLDOMElement}
  48. * @default null
  49. */
  50. this.element = null;
  51. /**
  52. * The camera used for raycasting.
  53. *
  54. * @type {?Camera}
  55. * @default null
  56. */
  57. this.camera = null;
  58. /**
  59. * An array of XR controllers.
  60. *
  61. * @type {Array<Group>}
  62. */
  63. this.controllers = [];
  64. this._onPointerEvent = this.onPointerEvent.bind( this );
  65. this._onXRControllerEvent = this.onXRControllerEvent.bind( this );
  66. }
  67. onPointerEvent( event ) {
  68. event.stopPropagation();
  69. const rect = this.element.getBoundingClientRect();
  70. _pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
  71. _pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
  72. this.raycaster.setFromCamera( _pointer, this.camera );
  73. const intersects = this.raycaster.intersectObjects( this.children, false );
  74. if ( intersects.length > 0 ) {
  75. const intersection = intersects[ 0 ];
  76. const object = intersection.object;
  77. const uv = intersection.uv;
  78. _event.type = event.type;
  79. _event.data.set( uv.x, 1 - uv.y );
  80. object.dispatchEvent( _event );
  81. }
  82. }
  83. onXRControllerEvent( event ) {
  84. const controller = event.target;
  85. _raycaster.setFromXRController( controller );
  86. const intersections = _raycaster.intersectObjects( this.children, false );
  87. if ( intersections.length > 0 ) {
  88. const intersection = intersections[ 0 ];
  89. const object = intersection.object;
  90. const uv = intersection.uv;
  91. _event.type = _events[ event.type ];
  92. _event.data.set( uv.x, 1 - uv.y );
  93. object.dispatchEvent( _event );
  94. }
  95. }
  96. /**
  97. * Calling this method makes sure the interactive group listens to Pointer and Mouse events.
  98. * The target is the `domElement` of the given renderer. The camera is required for the internal
  99. * raycasting so 3D objects can be detected based on the events.
  100. *
  101. * @param {(WebGPURenderer|WebGLRenderer)} renderer - The renderer.
  102. * @param {Camera} camera - The camera.
  103. */
  104. listenToPointerEvents( renderer, camera ) {
  105. this.camera = camera;
  106. this.element = renderer.domElement;
  107. this.element.addEventListener( 'pointerdown', this._onPointerEvent );
  108. this.element.addEventListener( 'pointerup', this._onPointerEvent );
  109. this.element.addEventListener( 'pointermove', this._onPointerEvent );
  110. this.element.addEventListener( 'mousedown', this._onPointerEvent );
  111. this.element.addEventListener( 'mouseup', this._onPointerEvent );
  112. this.element.addEventListener( 'mousemove', this._onPointerEvent );
  113. this.element.addEventListener( 'click', this._onPointerEvent );
  114. }
  115. /**
  116. * Disconnects this interactive group from all Pointer and Mouse Events.
  117. */
  118. disconnectionPointerEvents() {
  119. if ( this.element !== null ) {
  120. this.element.removeEventListener( 'pointerdown', this._onPointerEvent );
  121. this.element.removeEventListener( 'pointerup', this._onPointerEvent );
  122. this.element.removeEventListener( 'pointermove', this._onPointerEvent );
  123. this.element.removeEventListener( 'mousedown', this._onPointerEvent );
  124. this.element.removeEventListener( 'mouseup', this._onPointerEvent );
  125. this.element.removeEventListener( 'mousemove', this._onPointerEvent );
  126. this.element.removeEventListener( 'click', this._onPointerEvent );
  127. }
  128. }
  129. /**
  130. * Calling this method makes sure the interactive group listens to events of
  131. * the given XR controller.
  132. *
  133. * @param {Group} controller - The XR controller.
  134. */
  135. listenToXRControllerEvents( controller ) {
  136. this.controllers.push( controller );
  137. controller.addEventListener( 'move', this._onXRControllerEvent );
  138. controller.addEventListener( 'select', this._onXRControllerEvent );
  139. controller.addEventListener( 'selectstart', this._onXRControllerEvent );
  140. controller.addEventListener( 'selectend', this._onXRControllerEvent );
  141. }
  142. /**
  143. * Disconnects this interactive group from all XR controllers.
  144. */
  145. disconnectXrControllerEvents() {
  146. for ( const controller of this.controllers ) {
  147. controller.removeEventListener( 'move', this._onXRControllerEvent );
  148. controller.removeEventListener( 'select', this._onXRControllerEvent );
  149. controller.removeEventListener( 'selectstart', this._onXRControllerEvent );
  150. controller.removeEventListener( 'selectend', this._onXRControllerEvent );
  151. }
  152. }
  153. /**
  154. * Disconnects this interactive group from the DOM and all XR controllers.
  155. */
  156. disconnect() {
  157. this.disconnectionPointerEvents();
  158. this.disconnectXrControllerEvents();
  159. this.camera = null;
  160. this.element = null;
  161. this.controllers = [];
  162. }
  163. }
  164. export { InteractiveGroup };