DragControls.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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. }
  170. function onPointerMove( event ) {
  171. const camera = this.object;
  172. const domElement = this.domElement;
  173. const raycaster = this.raycaster;
  174. if ( this.enabled === false ) return;
  175. this._updatePointer( event );
  176. raycaster.setFromCamera( _pointer, camera );
  177. if ( _selected ) {
  178. if ( this.state === STATE.PAN ) {
  179. if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
  180. _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
  181. this.dispatchEvent( { type: 'drag', object: _selected } );
  182. }
  183. } else if ( this.state === STATE.ROTATE ) {
  184. _diff.subVectors( _pointer, _previousPointer ).multiplyScalar( this.rotateSpeed );
  185. _selected.rotateOnWorldAxis( _up, _diff.x );
  186. _selected.rotateOnWorldAxis( _right.normalize(), - _diff.y );
  187. this.dispatchEvent( { type: 'drag', object: _selected } );
  188. }
  189. _previousPointer.copy( _pointer );
  190. } else {
  191. // hover support
  192. if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
  193. _intersections.length = 0;
  194. raycaster.setFromCamera( _pointer, camera );
  195. raycaster.intersectObjects( this.objects, this.recursive, _intersections );
  196. if ( _intersections.length > 0 ) {
  197. const object = _intersections[ 0 ].object;
  198. _plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
  199. if ( _hovered !== object && _hovered !== null ) {
  200. this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
  201. domElement.style.cursor = 'auto';
  202. _hovered = null;
  203. }
  204. if ( _hovered !== object ) {
  205. this.dispatchEvent( { type: 'hoveron', object: object } );
  206. domElement.style.cursor = 'pointer';
  207. _hovered = object;
  208. }
  209. } else {
  210. if ( _hovered !== null ) {
  211. this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
  212. domElement.style.cursor = 'auto';
  213. _hovered = null;
  214. }
  215. }
  216. }
  217. }
  218. _previousPointer.copy( _pointer );
  219. }
  220. function onPointerDown( event ) {
  221. const camera = this.object;
  222. const domElement = this.domElement;
  223. const raycaster = this.raycaster;
  224. if ( this.enabled === false ) return;
  225. this._updatePointer( event );
  226. this._updateState( event );
  227. _intersections.length = 0;
  228. raycaster.setFromCamera( _pointer, camera );
  229. raycaster.intersectObjects( this.objects, this.recursive, _intersections );
  230. if ( _intersections.length > 0 ) {
  231. if ( this.transformGroup === true ) {
  232. // look for the outermost group in the object's upper hierarchy
  233. _selected = findGroup( _intersections[ 0 ].object );
  234. } else {
  235. _selected = _intersections[ 0 ].object;
  236. }
  237. _plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
  238. if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
  239. if ( this.state === STATE.PAN ) {
  240. _inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
  241. _offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
  242. domElement.style.cursor = 'move';
  243. this.dispatchEvent( { type: 'dragstart', object: _selected } );
  244. } else if ( this.state === STATE.ROTATE ) {
  245. // the controls only support Y+ up
  246. _up.set( 0, 1, 0 ).applyQuaternion( camera.quaternion ).normalize();
  247. _right.set( 1, 0, 0 ).applyQuaternion( camera.quaternion ).normalize();
  248. domElement.style.cursor = 'move';
  249. this.dispatchEvent( { type: 'dragstart', object: _selected } );
  250. }
  251. }
  252. }
  253. _previousPointer.copy( _pointer );
  254. }
  255. function onPointerCancel() {
  256. if ( this.enabled === false ) return;
  257. if ( _selected ) {
  258. this.dispatchEvent( { type: 'dragend', object: _selected } );
  259. _selected = null;
  260. }
  261. this.domElement.style.cursor = _hovered ? 'pointer' : 'auto';
  262. this.state = STATE.NONE;
  263. }
  264. function onContextMenu( event ) {
  265. if ( this.enabled === false ) return;
  266. event.preventDefault();
  267. }
  268. function findGroup( obj, group = null ) {
  269. if ( obj.isGroup ) group = obj;
  270. if ( obj.parent === null ) return group;
  271. return findGroup( obj.parent, group );
  272. }
  273. /**
  274. * Fires when the user drags a 3D object.
  275. *
  276. * @event DragControls#drag
  277. * @type {Object}
  278. */
  279. /**
  280. * Fires when the user has finished dragging a 3D object.
  281. *
  282. * @event DragControls#dragend
  283. * @type {Object}
  284. */
  285. /**
  286. * Fires when the pointer is moved onto a 3D object, or onto one of its children.
  287. *
  288. * @event DragControls#hoveron
  289. * @type {Object}
  290. */
  291. /**
  292. * Fires when the pointer is moved out of a 3D object.
  293. *
  294. * @event DragControls#hoveroff
  295. * @type {Object}
  296. */
  297. export { DragControls };
粤ICP备19079148号