DragControls.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. import {
  2. Controls,
  3. Matrix4,
  4. Plane,
  5. Raycaster,
  6. Vector2,
  7. Vector3,
  8. MOUSE,
  9. TOUCH
  10. } from 'three';
  11. const _plane = new Plane();
  12. const _pointer = new Vector2();
  13. const _offset = new Vector3();
  14. const _diff = new Vector2();
  15. const _previousPointer = new Vector2();
  16. const _intersection = new Vector3();
  17. const _worldPosition = new Vector3();
  18. const _inverseMatrix = new Matrix4();
  19. const _up = new Vector3();
  20. const _right = new Vector3();
  21. let _selected = null, _hovered = null;
  22. const _intersections = [];
  23. const STATE = {
  24. NONE: - 1,
  25. PAN: 0,
  26. ROTATE: 1
  27. };
  28. /**
  29. * This class can be used to provide a drag'n'drop interaction.
  30. *
  31. * ```js
  32. * const controls = new DragControls( objects, camera, renderer.domElement );
  33. *
  34. * // add event listener to highlight dragged objects
  35. * controls.addEventListener( 'dragstart', function ( event ) {
  36. *
  37. * event.object.material.emissive.set( 0xaaaaaa );
  38. *
  39. * } );
  40. *
  41. * controls.addEventListener( 'dragend', function ( event ) {
  42. *
  43. * event.object.material.emissive.set( 0x000000 );
  44. *
  45. * } );
  46. * ```
  47. *
  48. * @augments Controls
  49. * @three_import import { DragControls } from 'three/addons/controls/DragControls.js';
  50. */
  51. class DragControls extends Controls {
  52. /**
  53. * Constructs a new controls instance.
  54. *
  55. * @param {Array<Object3D>} objects - An array of draggable 3D objects.
  56. * @param {Camera} camera - The camera of the rendered scene.
  57. * @param {?HTMLDOMElement} [domElement=null] - The HTML DOM element used for event listeners.
  58. */
  59. constructor( objects, camera, domElement = null ) {
  60. super( camera, domElement );
  61. /**
  62. * An array of draggable 3D objects.
  63. *
  64. * @type {Array<Object3D>}
  65. */
  66. this.objects = objects;
  67. /**
  68. * Whether children of draggable objects can be dragged independently from their parent.
  69. *
  70. * @type {boolean}
  71. * @default true
  72. */
  73. this.recursive = true;
  74. /**
  75. * This option only works if the `objects` array contains a single draggable group object.
  76. * If set to `true`, the controls does not transform individual objects but the entire group.
  77. *
  78. * @type {boolean}
  79. * @default false
  80. */
  81. this.transformGroup = false;
  82. /**
  83. * The speed at which the object will rotate when dragged in `rotate` mode.
  84. * The higher the number the faster the rotation.
  85. *
  86. * @type {number}
  87. * @default 1
  88. */
  89. this.rotateSpeed = 1;
  90. /**
  91. * The raycaster used for detecting 3D objects.
  92. *
  93. * @type {Raycaster}
  94. */
  95. this.raycaster = new Raycaster();
  96. // interaction
  97. this.mouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.ROTATE };
  98. this.touches = { ONE: TOUCH.PAN };
  99. // event listeners
  100. this._onPointerMove = onPointerMove.bind( this );
  101. this._onPointerDown = onPointerDown.bind( this );
  102. this._onPointerCancel = onPointerCancel.bind( this );
  103. this._onContextMenu = onContextMenu.bind( this );
  104. //
  105. if ( domElement !== null ) {
  106. this.connect( domElement );
  107. }
  108. }
  109. connect( element ) {
  110. super.connect( element );
  111. this.domElement.addEventListener( 'pointermove', this._onPointerMove );
  112. this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
  113. this.domElement.addEventListener( 'pointerup', this._onPointerCancel );
  114. this.domElement.addEventListener( 'pointerleave', this._onPointerCancel );
  115. this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
  116. this.domElement.style.touchAction = 'none'; // disable touch scroll
  117. }
  118. disconnect() {
  119. this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
  120. this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
  121. this.domElement.removeEventListener( 'pointerup', this._onPointerCancel );
  122. this.domElement.removeEventListener( 'pointerleave', this._onPointerCancel );
  123. this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
  124. this.domElement.style.touchAction = 'auto';
  125. this.domElement.style.cursor = '';
  126. }
  127. dispose() {
  128. this.disconnect();
  129. }
  130. _updatePointer( event ) {
  131. const rect = this.domElement.getBoundingClientRect();
  132. _pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
  133. _pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
  134. }
  135. _updateState( event ) {
  136. // determine action
  137. let action;
  138. if ( event.pointerType === 'touch' ) {
  139. action = this.touches.ONE;
  140. } else {
  141. switch ( event.button ) {
  142. case 0:
  143. action = this.mouseButtons.LEFT;
  144. break;
  145. case 1:
  146. action = this.mouseButtons.MIDDLE;
  147. break;
  148. case 2:
  149. action = this.mouseButtons.RIGHT;
  150. break;
  151. default:
  152. action = null;
  153. }
  154. }
  155. // determine state
  156. switch ( action ) {
  157. case MOUSE.PAN:
  158. case TOUCH.PAN:
  159. this.state = STATE.PAN;
  160. break;
  161. case MOUSE.ROTATE:
  162. case TOUCH.ROTATE:
  163. this.state = STATE.ROTATE;
  164. break;
  165. default:
  166. this.state = STATE.NONE;
  167. }
  168. }
  169. getRaycaster() {
  170. console.warn( 'THREE.DragControls: getRaycaster() has been deprecated. Use controls.raycaster instead.' ); // @deprecated r169
  171. return this.raycaster;
  172. }
  173. setObjects( objects ) {
  174. console.warn( 'THREE.DragControls: setObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
  175. this.objects = objects;
  176. }
  177. getObjects() {
  178. console.warn( 'THREE.DragControls: getObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
  179. return this.objects;
  180. }
  181. activate() {
  182. console.warn( 'THREE.DragControls: activate() has been renamed to connect().' ); // @deprecated r169
  183. this.connect();
  184. }
  185. deactivate() {
  186. console.warn( 'THREE.DragControls: deactivate() has been renamed to disconnect().' ); // @deprecated r169
  187. this.disconnect();
  188. }
  189. set mode( value ) {
  190. console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
  191. }
  192. get mode() {
  193. console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
  194. }
  195. }
  196. function onPointerMove( event ) {
  197. const camera = this.object;
  198. const domElement = this.domElement;
  199. const raycaster = this.raycaster;
  200. if ( this.enabled === false ) return;
  201. this._updatePointer( event );
  202. raycaster.setFromCamera( _pointer, camera );
  203. if ( _selected ) {
  204. if ( this.state === STATE.PAN ) {
  205. if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
  206. _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
  207. }
  208. } else if ( this.state === STATE.ROTATE ) {
  209. _diff.subVectors( _pointer, _previousPointer ).multiplyScalar( this.rotateSpeed );
  210. _selected.rotateOnWorldAxis( _up, _diff.x );
  211. _selected.rotateOnWorldAxis( _right.normalize(), - _diff.y );
  212. }
  213. this.dispatchEvent( { type: 'drag', object: _selected } );
  214. _previousPointer.copy( _pointer );
  215. } else {
  216. // hover support
  217. if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
  218. _intersections.length = 0;
  219. raycaster.setFromCamera( _pointer, camera );
  220. raycaster.intersectObjects( this.objects, this.recursive, _intersections );
  221. if ( _intersections.length > 0 ) {
  222. const object = _intersections[ 0 ].object;
  223. _plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
  224. if ( _hovered !== object && _hovered !== null ) {
  225. this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
  226. domElement.style.cursor = 'auto';
  227. _hovered = null;
  228. }
  229. if ( _hovered !== object ) {
  230. this.dispatchEvent( { type: 'hoveron', object: object } );
  231. domElement.style.cursor = 'pointer';
  232. _hovered = object;
  233. }
  234. } else {
  235. if ( _hovered !== null ) {
  236. this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
  237. domElement.style.cursor = 'auto';
  238. _hovered = null;
  239. }
  240. }
  241. }
  242. }
  243. _previousPointer.copy( _pointer );
  244. }
  245. function onPointerDown( event ) {
  246. const camera = this.object;
  247. const domElement = this.domElement;
  248. const raycaster = this.raycaster;
  249. if ( this.enabled === false ) return;
  250. this._updatePointer( event );
  251. this._updateState( event );
  252. _intersections.length = 0;
  253. raycaster.setFromCamera( _pointer, camera );
  254. raycaster.intersectObjects( this.objects, this.recursive, _intersections );
  255. if ( _intersections.length > 0 ) {
  256. if ( this.transformGroup === true ) {
  257. // look for the outermost group in the object's upper hierarchy
  258. _selected = findGroup( _intersections[ 0 ].object );
  259. } else {
  260. _selected = _intersections[ 0 ].object;
  261. }
  262. _plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
  263. if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
  264. if ( this.state === STATE.PAN ) {
  265. _inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
  266. _offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
  267. } else if ( this.state === STATE.ROTATE ) {
  268. // the controls only support Y+ up
  269. _up.set( 0, 1, 0 ).applyQuaternion( camera.quaternion ).normalize();
  270. _right.set( 1, 0, 0 ).applyQuaternion( camera.quaternion ).normalize();
  271. }
  272. }
  273. domElement.style.cursor = 'move';
  274. this.dispatchEvent( { type: 'dragstart', object: _selected } );
  275. }
  276. _previousPointer.copy( _pointer );
  277. }
  278. function onPointerCancel() {
  279. if ( this.enabled === false ) return;
  280. if ( _selected ) {
  281. this.dispatchEvent( { type: 'dragend', object: _selected } );
  282. _selected = null;
  283. }
  284. this.domElement.style.cursor = _hovered ? 'pointer' : 'auto';
  285. this.state = STATE.NONE;
  286. }
  287. function onContextMenu( event ) {
  288. if ( this.enabled === false ) return;
  289. event.preventDefault();
  290. }
  291. function findGroup( obj, group = null ) {
  292. if ( obj.isGroup ) group = obj;
  293. if ( obj.parent === null ) return group;
  294. return findGroup( obj.parent, group );
  295. }
  296. /**
  297. * Fires when the user drags a 3D object.
  298. *
  299. * @event DragControls#drag
  300. * @type {Object}
  301. */
  302. /**
  303. * Fires when the user has finished dragging a 3D object.
  304. *
  305. * @event DragControls#dragend
  306. * @type {Object}
  307. */
  308. /**
  309. * Fires when the pointer is moved onto a 3D object, or onto one of its children.
  310. *
  311. * @event DragControls#hoveron
  312. * @type {Object}
  313. */
  314. /**
  315. * Fires when the pointer is moved out of a 3D object.
  316. *
  317. * @event DragControls#hoveroff
  318. * @type {Object}
  319. */
  320. export { DragControls };