Source: interaction/InteractionManager.js

interaction/InteractionManager.js

  1. import '../patch/EventDispatcher';
  2. import '../patch/Object3D';
  3. import { EventDispatcher, Raycaster } from 'three';
  4. import InteractionData from './InteractionData';
  5. import InteractionEvent from './InteractionEvent';
  6. import InteractionTrackingData from './InteractionTrackingData';
  7. const MOUSE_POINTER_ID = 'MOUSE';
  8. // helpers for hitTest() - only used inside hitTest()
  9. const hitTestEvent = {
  10. target: null,
  11. data: {
  12. global: null,
  13. },
  14. };
  15. /**
  16. * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive
  17. * if its interactive parameter is set to true
  18. * This manager also supports multitouch.
  19. *
  20. * reference to [pixi.js](http://www.pixijs.com/) impl
  21. *
  22. * @private
  23. * @class
  24. * @extends EventDispatcher
  25. */
  26. class InteractionManager extends EventDispatcher {
  27. /**
  28. * @param {WebGLRenderer} renderer - A reference to the current renderer
  29. * @param {Scene} scene - A reference to the current scene
  30. * @param {Camera} camera - A reference to the current camera
  31. * @param {Object} [options] - The options for the manager.
  32. * @param {Boolean} [options.autoPreventDefault=false] - Should the manager automatically prevent default browser actions.
  33. * @param {Boolean} [options.autoAttach=true] - Should the manager automatically attach target element.
  34. * @param {Number} [options.interactionFrequency=10] - Frequency increases the interaction events will be checked.
  35. */
  36. constructor(renderer, scene, camera, options) {
  37. super();
  38. options = options || {};
  39. /**
  40. * The renderer this interaction manager works for.
  41. *
  42. * @member {WebGLRenderer}
  43. */
  44. this.renderer = renderer;
  45. /**
  46. * The renderer this interaction manager works for.
  47. *
  48. * @member {Scene}
  49. */
  50. this.scene = scene;
  51. /**
  52. * The renderer this interaction manager works for.
  53. *
  54. * @member {Camera}
  55. */
  56. this.camera = camera;
  57. /**
  58. * Should default browser actions automatically be prevented.
  59. * Does not apply to pointer events for backwards compatibility
  60. * preventDefault on pointer events stops mouse events from firing
  61. * Thus, for every pointer event, there will always be either a mouse of touch event alongside it.
  62. *
  63. * @member {boolean}
  64. * @default false
  65. */
  66. this.autoPreventDefault = options.autoPreventDefault || false;
  67. /**
  68. * Frequency in milliseconds that the mousemove, moveover & mouseout interaction events will be checked.
  69. *
  70. * @member {number}
  71. * @default 10
  72. */
  73. this.interactionFrequency = options.interactionFrequency || 10;
  74. /**
  75. * The mouse data
  76. *
  77. * @member {InteractionData}
  78. */
  79. this.mouse = new InteractionData();
  80. this.mouse.identifier = MOUSE_POINTER_ID;
  81. // setting the mouse to start off far off screen will mean that mouse over does
  82. // not get called before we even move the mouse.
  83. this.mouse.global.set(-999999);
  84. /**
  85. * Actively tracked InteractionData
  86. *
  87. * @private
  88. * @member {Object.<number,InteractionData>}
  89. */
  90. this.activeInteractionData = {};
  91. this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse;
  92. /**
  93. * Pool of unused InteractionData
  94. *
  95. * @private
  96. * @member {InteractionData[]}
  97. */
  98. this.interactionDataPool = [];
  99. /**
  100. * An event data object to handle all the event tracking/dispatching
  101. *
  102. * @member {object}
  103. */
  104. this.eventData = new InteractionEvent();
  105. /**
  106. * The DOM element to bind to.
  107. *
  108. * @private
  109. * @member {HTMLElement}
  110. */
  111. this.interactionDOMElement = null;
  112. /**
  113. * This property determines if mousemove and touchmove events are fired only when the cursor
  114. * is over the object.
  115. * Setting to true will make things work more in line with how the DOM verison works.
  116. * Setting to false can make things easier for things like dragging
  117. * It is currently set to false as this is how three.js used to work.
  118. *
  119. * @member {boolean}
  120. * @default true
  121. */
  122. this.moveWhenInside = true;
  123. /**
  124. * Have events been attached to the dom element?
  125. *
  126. * @private
  127. * @member {boolean}
  128. */
  129. this.eventsAdded = false;
  130. /**
  131. * Is the mouse hovering over the renderer?
  132. *
  133. * @private
  134. * @member {boolean}
  135. */
  136. this.mouseOverRenderer = false;
  137. /**
  138. * Does the device support touch events
  139. * https://www.w3.org/TR/touch-events/
  140. *
  141. * @readonly
  142. * @member {boolean}
  143. */
  144. this.supportsTouchEvents = 'ontouchstart' in window;
  145. /**
  146. * Does the device support pointer events
  147. * https://www.w3.org/Submission/pointer-events/
  148. *
  149. * @readonly
  150. * @member {boolean}
  151. */
  152. this.supportsPointerEvents = !!window.PointerEvent;
  153. // this will make it so that you don't have to call bind all the time
  154. /**
  155. * @private
  156. * @member {Function}
  157. */
  158. this.onClick = this.onClick.bind(this);
  159. this.processClick = this.processClick.bind(this);
  160. /**
  161. * @private
  162. * @member {Function}
  163. */
  164. this.onPointerUp = this.onPointerUp.bind(this);
  165. this.processPointerUp = this.processPointerUp.bind(this);
  166. /**
  167. * @private
  168. * @member {Function}
  169. */
  170. this.onPointerCancel = this.onPointerCancel.bind(this);
  171. this.processPointerCancel = this.processPointerCancel.bind(this);
  172. /**
  173. * @private
  174. * @member {Function}
  175. */
  176. this.onPointerDown = this.onPointerDown.bind(this);
  177. this.processPointerDown = this.processPointerDown.bind(this);
  178. /**
  179. * @private
  180. * @member {Function}
  181. */
  182. this.onPointerMove = this.onPointerMove.bind(this);
  183. this.processPointerMove = this.processPointerMove.bind(this);
  184. /**
  185. * @private
  186. * @member {Function}
  187. */
  188. this.onPointerOut = this.onPointerOut.bind(this);
  189. this.processPointerOverOut = this.processPointerOverOut.bind(this);
  190. /**
  191. * @private
  192. * @member {Function}
  193. */
  194. this.onPointerOver = this.onPointerOver.bind(this);
  195. /**
  196. * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor
  197. * values, objects are handled as dictionaries of CSS values for interactionDOMElement,
  198. * and functions are called instead of changing the CSS.
  199. * Default CSS cursor values are provided for 'default' and 'pointer' modes.
  200. * @member {Object.<string, (string|Function|Object.<string, string>)>}
  201. */
  202. this.cursorStyles = {
  203. default: 'inherit',
  204. pointer: 'pointer',
  205. };
  206. /**
  207. * The mode of the cursor that is being used.
  208. * The value of this is a key from the cursorStyles dictionary.
  209. *
  210. * @member {string}
  211. */
  212. this.currentCursorMode = null;
  213. /**
  214. * Internal cached let.
  215. *
  216. * @private
  217. * @member {string}
  218. */
  219. this.cursor = null;
  220. /**
  221. * ray caster, for survey intersects from 3d-scene
  222. *
  223. * @private
  224. * @member {Raycaster}
  225. */
  226. this.raycaster = new Raycaster();
  227. /**
  228. * snippet time
  229. *
  230. * @private
  231. * @member {Number}
  232. */
  233. this._deltaTime = 0;
  234. this.setTargetElement(this.renderer.domElement);
  235. /**
  236. * Fired when a pointer device button (usually a mouse left-button) is pressed on the display
  237. * object.
  238. *
  239. * @event InteractionManager#mousedown
  240. * @param {InteractionEvent} event - Interaction event
  241. */
  242. /**
  243. * Fired when a pointer device secondary button (usually a mouse right-button) is pressed
  244. * on the display object.
  245. *
  246. * @event InteractionManager#rightdown
  247. * @param {InteractionEvent} event - Interaction event
  248. */
  249. /**
  250. * Fired when a pointer device button (usually a mouse left-button) is released over the display
  251. * object.
  252. *
  253. * @event InteractionManager#mouseup
  254. * @param {InteractionEvent} event - Interaction event
  255. */
  256. /**
  257. * Fired when a pointer device secondary button (usually a mouse right-button) is released
  258. * over the display object.
  259. *
  260. * @event InteractionManager#rightup
  261. * @param {InteractionEvent} event - Interaction event
  262. */
  263. /**
  264. * Fired when a pointer device button (usually a mouse left-button) is pressed and released on
  265. * the display object.
  266. *
  267. * @event InteractionManager#click
  268. * @param {InteractionEvent} event - Interaction event
  269. */
  270. /**
  271. * Fired when a pointer device secondary button (usually a mouse right-button) is pressed
  272. * and released on the display object.
  273. *
  274. * @event InteractionManager#rightclick
  275. * @param {InteractionEvent} event - Interaction event
  276. */
  277. /**
  278. * Fired when a pointer device button (usually a mouse left-button) is released outside the
  279. * display object that initially registered a
  280. * [mousedown]{@link InteractionManager#event:mousedown}.
  281. *
  282. * @event InteractionManager#mouseupoutside
  283. * @param {InteractionEvent} event - Interaction event
  284. */
  285. /**
  286. * Fired when a pointer device secondary button (usually a mouse right-button) is released
  287. * outside the display object that initially registered a
  288. * [rightdown]{@link InteractionManager#event:rightdown}.
  289. *
  290. * @event InteractionManager#rightupoutside
  291. * @param {InteractionEvent} event - Interaction event
  292. */
  293. /**
  294. * Fired when a pointer device (usually a mouse) is moved while over the display object
  295. *
  296. * @event InteractionManager#mousemove
  297. * @param {InteractionEvent} event - Interaction event
  298. */
  299. /**
  300. * Fired when a pointer device (usually a mouse) is moved onto the display object
  301. *
  302. * @event InteractionManager#mouseover
  303. * @param {InteractionEvent} event - Interaction event
  304. */
  305. /**
  306. * Fired when a pointer device (usually a mouse) is moved off the display object
  307. *
  308. * @event InteractionManager#mouseout
  309. * @param {InteractionEvent} event - Interaction event
  310. */
  311. /**
  312. * Fired when a pointer device button is pressed on the display object.
  313. *
  314. * @event InteractionManager#pointerdown
  315. * @param {InteractionEvent} event - Interaction event
  316. */
  317. /**
  318. * Fired when a pointer device button is released over the display object.
  319. *
  320. * @event InteractionManager#pointerup
  321. * @param {InteractionEvent} event - Interaction event
  322. */
  323. /**
  324. * Fired when the operating system cancels a pointer event
  325. *
  326. * @event InteractionManager#pointercancel
  327. * @param {InteractionEvent} event - Interaction event
  328. */
  329. /**
  330. * Fired when a pointer device button is pressed and released on the display object.
  331. *
  332. * @event InteractionManager#pointertap
  333. * @param {InteractionEvent} event - Interaction event
  334. */
  335. /**
  336. * Fired when a pointer device button is released outside the display object that initially
  337. * registered a [pointerdown]{@link InteractionManager#event:pointerdown}.
  338. *
  339. * @event InteractionManager#pointerupoutside
  340. * @param {InteractionEvent} event - Interaction event
  341. */
  342. /**
  343. * Fired when a pointer device is moved while over the display object
  344. *
  345. * @event InteractionManager#pointermove
  346. * @param {InteractionEvent} event - Interaction event
  347. */
  348. /**
  349. * Fired when a pointer device is moved onto the display object
  350. *
  351. * @event InteractionManager#pointerover
  352. * @param {InteractionEvent} event - Interaction event
  353. */
  354. /**
  355. * Fired when a pointer device is moved off the display object
  356. *
  357. * @event InteractionManager#pointerout
  358. * @param {InteractionEvent} event - Interaction event
  359. */
  360. /**
  361. * Fired when a touch point is placed on the display object.
  362. *
  363. * @event InteractionManager#touchstart
  364. * @param {InteractionEvent} event - Interaction event
  365. */
  366. /**
  367. * Fired when a touch point is removed from the display object.
  368. *
  369. * @event InteractionManager#touchend
  370. * @param {InteractionEvent} event - Interaction event
  371. */
  372. /**
  373. * Fired when the operating system cancels a touch
  374. *
  375. * @event InteractionManager#touchcancel
  376. * @param {InteractionEvent} event - Interaction event
  377. */
  378. /**
  379. * Fired when a touch point is placed and removed from the display object.
  380. *
  381. * @event InteractionManager#tap
  382. * @param {InteractionEvent} event - Interaction event
  383. */
  384. /**
  385. * Fired when a touch point is removed outside of the display object that initially
  386. * registered a [touchstart]{@link InteractionManager#event:touchstart}.
  387. *
  388. * @event InteractionManager#touchendoutside
  389. * @param {InteractionEvent} event - Interaction event
  390. */
  391. /**
  392. * Fired when a touch point is moved along the display object.
  393. *
  394. * @event InteractionManager#touchmove
  395. * @param {InteractionEvent} event - Interaction event
  396. */
  397. /**
  398. * Fired when a pointer device button (usually a mouse left-button) is pressed on the display.
  399. * object. DisplayObject's `interactive` property must be set to `true` to fire event.
  400. *
  401. * @event Object3D#mousedown
  402. * @param {InteractionEvent} event - Interaction event
  403. */
  404. /**
  405. * Fired when a pointer device secondary button (usually a mouse right-button) is pressed
  406. * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event.
  407. *
  408. * @event Object3D#rightdown
  409. * @param {InteractionEvent} event - Interaction event
  410. */
  411. /**
  412. * Fired when a pointer device button (usually a mouse left-button) is released over the display
  413. * object. DisplayObject's `interactive` property must be set to `true` to fire event.
  414. *
  415. * @event Object3D#mouseup
  416. * @param {InteractionEvent} event - Interaction event
  417. */
  418. /**
  419. * Fired when a pointer device secondary button (usually a mouse right-button) is released
  420. * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event.
  421. *
  422. * @event Object3D#rightup
  423. * @param {InteractionEvent} event - Interaction event
  424. */
  425. /**
  426. * Fired when a pointer device button (usually a mouse left-button) is pressed and released on
  427. * the display object. DisplayObject's `interactive` property must be set to `true` to fire event.
  428. *
  429. * @event Object3D#click
  430. * @param {InteractionEvent} event - Interaction event
  431. */
  432. /**
  433. * Fired when a pointer device secondary button (usually a mouse right-button) is pressed
  434. * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event.
  435. *
  436. * @event Object3D#rightclick
  437. * @param {InteractionEvent} event - Interaction event
  438. */
  439. /**
  440. * Fired when a pointer device button (usually a mouse left-button) is released outside the
  441. * display object that initially registered a
  442. * [mousedown]{@link Object3D#event:mousedown}.
  443. * DisplayObject's `interactive` property must be set to `true` to fire event.
  444. *
  445. * @event Object3D#mouseupoutside
  446. * @param {InteractionEvent} event - Interaction event
  447. */
  448. /**
  449. * Fired when a pointer device secondary button (usually a mouse right-button) is released
  450. * outside the display object that initially registered a
  451. * [rightdown]{@link Object3D#event:rightdown}.
  452. * DisplayObject's `interactive` property must be set to `true` to fire event.
  453. *
  454. * @event Object3D#rightupoutside
  455. * @param {InteractionEvent} event - Interaction event
  456. */
  457. /**
  458. * Fired when a pointer device (usually a mouse) is moved while over the display object.
  459. * DisplayObject's `interactive` property must be set to `true` to fire event.
  460. *
  461. * @event Object3D#mousemove
  462. * @param {InteractionEvent} event - Interaction event
  463. */
  464. /**
  465. * Fired when a pointer device (usually a mouse) is moved onto the display object.
  466. * DisplayObject's `interactive` property must be set to `true` to fire event.
  467. *
  468. * @event Object3D#mouseover
  469. * @param {InteractionEvent} event - Interaction event
  470. */
  471. /**
  472. * Fired when a pointer device (usually a mouse) is moved off the display object.
  473. * DisplayObject's `interactive` property must be set to `true` to fire event.
  474. *
  475. * @event Object3D#mouseout
  476. * @param {InteractionEvent} event - Interaction event
  477. */
  478. /**
  479. * Fired when a pointer device button is pressed on the display object.
  480. * DisplayObject's `interactive` property must be set to `true` to fire event.
  481. *
  482. * @event Object3D#pointerdown
  483. * @param {InteractionEvent} event - Interaction event
  484. */
  485. /**
  486. * Fired when a pointer device button is released over the display object.
  487. * DisplayObject's `interactive` property must be set to `true` to fire event.
  488. *
  489. * @event Object3D#pointerup
  490. * @param {InteractionEvent} event - Interaction event
  491. */
  492. /**
  493. * Fired when the operating system cancels a pointer event.
  494. * DisplayObject's `interactive` property must be set to `true` to fire event.
  495. *
  496. * @event Object3D#pointercancel
  497. * @param {InteractionEvent} event - Interaction event
  498. */
  499. /**
  500. * Fired when a pointer device button is pressed and released on the display object.
  501. * DisplayObject's `interactive` property must be set to `true` to fire event.
  502. *
  503. * @event Object3D#pointertap
  504. * @param {InteractionEvent} event - Interaction event
  505. */
  506. /**
  507. * Fired when a pointer device button is released outside the display object that initially
  508. * registered a [pointerdown]{@link Object3D#event:pointerdown}.
  509. * DisplayObject's `interactive` property must be set to `true` to fire event.
  510. *
  511. * @event Object3D#pointerupoutside
  512. * @param {InteractionEvent} event - Interaction event
  513. */
  514. /**
  515. * Fired when a pointer device is moved while over the display object.
  516. * DisplayObject's `interactive` property must be set to `true` to fire event.
  517. *
  518. * @event Object3D#pointermove
  519. * @param {InteractionEvent} event - Interaction event
  520. */
  521. /**
  522. * Fired when a pointer device is moved onto the display object.
  523. * DisplayObject's `interactive` property must be set to `true` to fire event.
  524. *
  525. * @event Object3D#pointerover
  526. * @param {InteractionEvent} event - Interaction event
  527. */
  528. /**
  529. * Fired when a pointer device is moved off the display object.
  530. * DisplayObject's `interactive` property must be set to `true` to fire event.
  531. *
  532. * @event Object3D#pointerout
  533. * @param {InteractionEvent} event - Interaction event
  534. */
  535. /**
  536. * Fired when a touch point is placed on the display object.
  537. * DisplayObject's `interactive` property must be set to `true` to fire event.
  538. *
  539. * @event Object3D#touchstart
  540. * @param {InteractionEvent} event - Interaction event
  541. */
  542. /**
  543. * Fired when a touch point is removed from the display object.
  544. * DisplayObject's `interactive` property must be set to `true` to fire event.
  545. *
  546. * @event Object3D#touchend
  547. * @param {InteractionEvent} event - Interaction event
  548. */
  549. /**
  550. * Fired when the operating system cancels a touch.
  551. * DisplayObject's `interactive` property must be set to `true` to fire event.
  552. *
  553. * @event Object3D#touchcancel
  554. * @param {InteractionEvent} event - Interaction event
  555. */
  556. /**
  557. * Fired when a touch point is placed and removed from the display object.
  558. * DisplayObject's `interactive` property must be set to `true` to fire event.
  559. *
  560. * @event Object3D#tap
  561. * @param {InteractionEvent} event - Interaction event
  562. */
  563. /**
  564. * Fired when a touch point is removed outside of the display object that initially
  565. * registered a [touchstart]{@link Object3D#event:touchstart}.
  566. * DisplayObject's `interactive` property must be set to `true` to fire event.
  567. *
  568. * @event Object3D#touchendoutside
  569. * @param {InteractionEvent} event - Interaction event
  570. */
  571. /**
  572. * Fired when a touch point is moved along the display object.
  573. * DisplayObject's `interactive` property must be set to `true` to fire event.
  574. *
  575. * @event Object3D#touchmove
  576. * @param {InteractionEvent} event - Interaction event
  577. */
  578. }
  579. /**
  580. * Hit tests a point against the display tree, returning the first interactive object that is hit.
  581. *
  582. * @param {Point} globalPoint - A point to hit test with, in global space.
  583. * @param {Object3D} [root] - The root display object to start from. If omitted, defaults
  584. * to the last rendered root of the associated renderer.
  585. * @return {Object3D} The hit display object, if any.
  586. */
  587. hitTest(globalPoint, root) {
  588. // clear the target for our hit test
  589. hitTestEvent.target = null;
  590. // assign the global point
  591. hitTestEvent.data.global = globalPoint;
  592. // ensure safety of the root
  593. if (!root) {
  594. root = this.scene;
  595. }
  596. // run the hit test
  597. this.processInteractive(hitTestEvent, root, null, true);
  598. // return our found object - it'll be null if we didn't hit anything
  599. return hitTestEvent.target;
  600. }
  601. /**
  602. * Sets the DOM element which will receive mouse/touch events. This is useful for when you have
  603. * other DOM elements on top of the renderers Canvas element. With this you'll be bale to deletegate
  604. * another DOM element to receive those events.
  605. *
  606. * @param {HTMLCanvasElement} element - the DOM element which will receive mouse and touch events.
  607. */
  608. setTargetElement(element) {
  609. this.removeEvents();
  610. this.interactionDOMElement = element;
  611. this.addEvents();
  612. }
  613. /**
  614. * Registers all the DOM events
  615. *
  616. * @private
  617. */
  618. addEvents() {
  619. if (!this.interactionDOMElement || this.eventsAdded) {
  620. return;
  621. }
  622. this.emit('addevents');
  623. this.interactionDOMElement.addEventListener('click', this.onClick, true);
  624. if (window.navigator.msPointerEnabled) {
  625. this.interactionDOMElement.style['-ms-content-zooming'] = 'none';
  626. this.interactionDOMElement.style['-ms-touch-action'] = 'none';
  627. } else if (this.supportsPointerEvents) {
  628. this.interactionDOMElement.style['touch-action'] = 'none';
  629. }
  630. /**
  631. * These events are added first, so that if pointer events are normalised, they are fired
  632. * in the same order as non-normalised events. ie. pointer event 1st, mouse / touch 2nd
  633. */
  634. if (this.supportsPointerEvents) {
  635. window.document.addEventListener('pointermove', this.onPointerMove, true);
  636. this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true);
  637. // pointerout is fired in addition to pointerup (for touch events) and pointercancel
  638. // we already handle those, so for the purposes of what we do in onPointerOut, we only
  639. // care about the pointerleave event
  640. this.interactionDOMElement.addEventListener('pointerleave', this.onPointerOut, true);
  641. this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true);
  642. window.addEventListener('pointercancel', this.onPointerCancel, true);
  643. window.addEventListener('pointerup', this.onPointerUp, true);
  644. } else {
  645. window.document.addEventListener('mousemove', this.onPointerMove, true);
  646. this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true);
  647. this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true);
  648. this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true);
  649. window.addEventListener('mouseup', this.onPointerUp, true);
  650. }
  651. // always look directly for touch events so that we can provide original data
  652. // In a future version we should change this to being just a fallback and rely solely on
  653. // PointerEvents whenever available
  654. if (this.supportsTouchEvents) {
  655. this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true);
  656. this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true);
  657. this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true);
  658. this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true);
  659. }
  660. this.eventsAdded = true;
  661. }
  662. /**
  663. * Removes all the DOM events that were previously registered
  664. *
  665. * @private
  666. */
  667. removeEvents() {
  668. if (!this.interactionDOMElement) {
  669. return;
  670. }
  671. this.emit('removeevents');
  672. this.interactionDOMElement.removeEventListener('click', this.onClick, true);
  673. if (window.navigator.msPointerEnabled) {
  674. this.interactionDOMElement.style['-ms-content-zooming'] = '';
  675. this.interactionDOMElement.style['-ms-touch-action'] = '';
  676. } else if (this.supportsPointerEvents) {
  677. this.interactionDOMElement.style['touch-action'] = '';
  678. }
  679. if (this.supportsPointerEvents) {
  680. window.document.removeEventListener('pointermove', this.onPointerMove, true);
  681. this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true);
  682. this.interactionDOMElement.removeEventListener('pointerleave', this.onPointerOut, true);
  683. this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true);
  684. window.removeEventListener('pointercancel', this.onPointerCancel, true);
  685. window.removeEventListener('pointerup', this.onPointerUp, true);
  686. } else {
  687. window.document.removeEventListener('mousemove', this.onPointerMove, true);
  688. this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true);
  689. this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true);
  690. this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true);
  691. window.removeEventListener('mouseup', this.onPointerUp, true);
  692. }
  693. if (this.supportsTouchEvents) {
  694. this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true);
  695. this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true);
  696. this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true);
  697. this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true);
  698. }
  699. this.interactionDOMElement = null;
  700. this.eventsAdded = false;
  701. }
  702. /**
  703. * Updates the state of interactive objects.
  704. * Invoked by a throttled ticker.
  705. *
  706. * @param {number} deltaTime - time delta since last tick
  707. */
  708. update({ snippet }) {
  709. this._deltaTime += snippet;
  710. if (this._deltaTime < this.interactionFrequency) {
  711. return;
  712. }
  713. this._deltaTime = 0;
  714. if (!this.interactionDOMElement) {
  715. return;
  716. }
  717. // if the user move the mouse this check has already been done using the mouse move!
  718. if (this.didMove) {
  719. this.didMove = false;
  720. return;
  721. }
  722. this.cursor = null;
  723. // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind,
  724. // but there was a scenario of a display object moving under a static mouse cursor.
  725. // In this case, mouseover and mouseevents would not pass the flag test in triggerEvent function
  726. for (const k in this.activeInteractionData) {
  727. // eslint-disable-next-line no-prototype-builtins
  728. if (this.activeInteractionData.hasOwnProperty(k)) {
  729. const interactionData = this.activeInteractionData[k];
  730. if (interactionData.originalEvent && interactionData.pointerType !== 'touch') {
  731. const interactionEvent = this.configureInteractionEventForDOMEvent(
  732. this.eventData,
  733. interactionData.originalEvent,
  734. interactionData
  735. );
  736. this.processInteractive(
  737. interactionEvent,
  738. this.scene,
  739. this.processPointerOverOut,
  740. true
  741. );
  742. }
  743. }
  744. }
  745. this.setCursorMode(this.cursor);
  746. // TODO
  747. }
  748. /**
  749. * Sets the current cursor mode, handling any callbacks or CSS style changes.
  750. *
  751. * @param {string} mode - cursor mode, a key from the cursorStyles dictionary
  752. */
  753. setCursorMode(mode) {
  754. mode = mode || 'default';
  755. // if the mode didn't actually change, bail early
  756. if (this.currentCursorMode === mode) {
  757. return;
  758. }
  759. this.currentCursorMode = mode;
  760. const style = this.cursorStyles[mode];
  761. // only do things if there is a cursor style for it
  762. if (style) {
  763. switch (typeof style) {
  764. case 'string':
  765. // string styles are handled as cursor CSS
  766. this.interactionDOMElement.style.cursor = style;
  767. break;
  768. case 'function':
  769. // functions are just called, and passed the cursor mode
  770. style(mode);
  771. break;
  772. case 'object':
  773. // if it is an object, assume that it is a dictionary of CSS styles,
  774. // apply it to the interactionDOMElement
  775. Object.assign(this.interactionDOMElement.style, style);
  776. break;
  777. default:
  778. break;
  779. }
  780. } else if (typeof mode === 'string' && !Object.prototype.hasOwnProperty.call(this.cursorStyles, mode)) {
  781. // if it mode is a string (not a Symbol) and cursorStyles doesn't have any entry
  782. // for the mode, then assume that the dev wants it to be CSS for the cursor.
  783. this.interactionDOMElement.style.cursor = mode;
  784. }
  785. }
  786. /**
  787. * Dispatches an event on the display object that was interacted with
  788. *
  789. * @param {Object3D} displayObject - the display object in question
  790. * @param {string} eventString - the name of the event (e.g, mousedown)
  791. * @param {object} eventData - the event data object
  792. * @private
  793. */
  794. triggerEvent(displayObject, eventString, eventData) {
  795. if (!eventData.stopped) {
  796. eventData.currentTarget = displayObject;
  797. eventData.type = eventString;
  798. displayObject.emit(eventString, eventData);
  799. if (displayObject[eventString]) {
  800. displayObject[eventString](eventData);
  801. }
  802. }
  803. }
  804. /**
  805. * This function is provides a neat way of crawling through the scene graph and running a
  806. * specified function on all interactive objects it finds. It will also take care of hit
  807. * testing the interactive objects and passes the hit across in the function.
  808. *
  809. * @private
  810. * @param {InteractionEvent} interactionEvent - event containing the point that
  811. * is tested for collision
  812. * @param {Object3D} displayObject - the displayObject
  813. * that will be hit test (recursively crawls its children)
  814. * @param {Function} [func] - the function that will be called on each interactive object. The
  815. * interactionEvent, displayObject and hit will be passed to the function
  816. * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point
  817. * @param {boolean} [interactive] - Whether the displayObject is interactive
  818. * @return {boolean} returns true if the displayObject hit the point
  819. */
  820. processInteractive(interactionEvent, displayObject, func, hitTest, interactive) {
  821. if (!displayObject || !displayObject.visible) {
  822. return false;
  823. }
  824. // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^
  825. //
  826. // This function will now loop through all objects and then only hit test the objects it HAS
  827. // to, not all of them. MUCH faster..
  828. // An object will be hit test if the following is true:
  829. //
  830. // 1: It is interactive.
  831. // 2: It belongs to a parent that is interactive AND one of the parents children have not already been hit.
  832. //
  833. // As another little optimisation once an interactive object has been hit we can carry on
  834. // through the scenegraph, but we know that there will be no more hits! So we can avoid extra hit tests
  835. // A final optimisation is that an object is not hit test directly if a child has already been hit.
  836. interactive = displayObject.interactive || interactive;
  837. let hit = false;
  838. let interactiveParent = interactive;
  839. if (displayObject.interactiveChildren && displayObject.children) {
  840. const children = displayObject.children;
  841. for (let i = children.length - 1; i >= 0; i--) {
  842. const child = children[i];
  843. // time to get recursive.. if this function will return if something is hit..
  844. const childHit = this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent);
  845. if (childHit) {
  846. // its a good idea to check if a child has lost its parent.
  847. // this means it has been removed whilst looping so its best
  848. if (!child.parent) {
  849. continue;
  850. }
  851. // we no longer need to hit test any more objects in this container as we we
  852. // now know the parent has been hit
  853. interactiveParent = false;
  854. // If the child is interactive , that means that the object hit was actually
  855. // interactive and not just the child of an interactive object.
  856. // This means we no longer need to hit test anything else. We still need to run
  857. // through all objects, but we don't need to perform any hit tests.
  858. if (childHit) {
  859. if (interactionEvent.target) {
  860. hitTest = false;
  861. }
  862. hit = true;
  863. }
  864. }
  865. }
  866. }
  867. // no point running this if the item is not interactive or does not have an interactive parent.
  868. if (interactive) {
  869. // if we are hit testing (as in we have no hit any objects yet)
  870. // We also don't need to worry about hit testing if once of the displayObjects children
  871. // has already been hit - but only if it was interactive, otherwise we need to keep
  872. // looking for an interactive child, just in case we hit one
  873. if (hitTest && !interactionEvent.target) {
  874. if (interactionEvent.intersects[0] && interactionEvent.intersects[0].object === displayObject) {
  875. hit = true;
  876. }
  877. }
  878. if (displayObject.interactive) {
  879. if (hit && !interactionEvent.target) {
  880. interactionEvent.data.target = interactionEvent.target = displayObject;
  881. }
  882. if (func) {
  883. func(interactionEvent, displayObject, !!hit);
  884. }
  885. }
  886. }
  887. return hit;
  888. }
  889. /**
  890. * Is called when the click is pressed down on the renderer element
  891. *
  892. * @private
  893. * @param {MouseEvent} originalEvent - The DOM event of a click being pressed down
  894. */
  895. onClick(originalEvent) {
  896. if (originalEvent.type !== 'click') return;
  897. const events = this.normalizeToPointerData(originalEvent);
  898. if (this.autoPreventDefault && events[0].isNormalized) {
  899. originalEvent.preventDefault();
  900. }
  901. const interactionData = this.getInteractionDataForPointerId(events[0]);
  902. const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, events[0], interactionData);
  903. interactionEvent.data.originalEvent = originalEvent;
  904. this.processInteractive(interactionEvent, this.scene, this.processClick, true);
  905. this.emit('click', interactionEvent);
  906. }
  907. /**
  908. * Processes the result of the click check and dispatches the event if need be
  909. *
  910. * @private
  911. * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event
  912. * @param {Object3D} displayObject - The display object that was tested
  913. * @param {boolean} hit - the result of the hit test on the display object
  914. */
  915. processClick(interactionEvent, displayObject, hit) {
  916. if (hit) {
  917. this.triggerEvent(displayObject, 'click', interactionEvent);
  918. }
  919. }
  920. /**
  921. * Is called when the pointer button is pressed down on the renderer element
  922. *
  923. * @private
  924. * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down
  925. */
  926. onPointerDown(originalEvent) {
  927. // if we support touch events, then only use those for touch events, not pointer events
  928. if (this.supportsTouchEvents && originalEvent.pointerType === 'touch') return;
  929. const events = this.normalizeToPointerData(originalEvent);
  930. /**
  931. * No need to prevent default on natural pointer events, as there are no side effects
  932. * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser,
  933. * so still need to be prevented.
  934. */
  935. // Guaranteed that there will be at least one event in events, and all events must have the same pointer type
  936. if (this.autoPreventDefault && events[0].isNormalized) {
  937. originalEvent.preventDefault();
  938. }
  939. const eventLen = events.length;
  940. for (let i = 0; i < eventLen; i++) {
  941. const event = events[i];
  942. const interactionData = this.getInteractionDataForPointerId(event);
  943. const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData);
  944. interactionEvent.data.originalEvent = originalEvent;
  945. this.processInteractive(interactionEvent, this.scene, this.processPointerDown, true);
  946. this.emit('pointerdown', interactionEvent);
  947. if (event.pointerType === 'touch') {
  948. this.emit('touchstart', interactionEvent);
  949. } else if (event.pointerType === 'mouse' || event.pointerType === 'pen') {
  950. const isRightButton = event.button === 2;
  951. this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData);
  952. }
  953. }
  954. }
  955. /**
  956. * Processes the result of the pointer down check and dispatches the event if need be
  957. *
  958. * @private
  959. * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event
  960. * @param {Object3D} displayObject - The display object that was tested
  961. * @param {boolean} hit - the result of the hit test on the display object
  962. */
  963. processPointerDown(interactionEvent, displayObject, hit) {
  964. const data = interactionEvent.data;
  965. const id = interactionEvent.data.identifier;
  966. if (hit) {
  967. if (!displayObject.trackedPointers[id]) {
  968. displayObject.trackedPointers[id] = new InteractionTrackingData(id);
  969. }
  970. this.triggerEvent(displayObject, 'pointerdown', interactionEvent);
  971. if (data.pointerType === 'touch') {
  972. displayObject.started = true;
  973. this.triggerEvent(displayObject, 'touchstart', interactionEvent);
  974. } else if (data.pointerType === 'mouse' || data.pointerType === 'pen') {
  975. const isRightButton = data.button === 2;
  976. if (isRightButton) {
  977. displayObject.trackedPointers[id].rightDown = true;
  978. } else {
  979. displayObject.trackedPointers[id].leftDown = true;
  980. }
  981. this.triggerEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent);
  982. }
  983. }
  984. }
  985. /**
  986. * Is called when the pointer button is released on the renderer element
  987. *
  988. * @private
  989. * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released
  990. * @param {boolean} cancelled - true if the pointer is cancelled
  991. * @param {Function} func - Function passed to {@link processInteractive}
  992. */
  993. onPointerComplete(originalEvent, cancelled, func) {
  994. const events = this.normalizeToPointerData(originalEvent);
  995. const eventLen = events.length;
  996. // if the event wasn't targeting our canvas, then consider it to be pointerupoutside
  997. // in all cases (unless it was a pointercancel)
  998. const eventAppend = originalEvent.target !== this.interactionDOMElement ? 'outside' : '';
  999. for (let i = 0; i < eventLen; i++) {
  1000. const event = events[i];
  1001. const interactionData = this.getInteractionDataForPointerId(event);
  1002. const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData);
  1003. interactionEvent.data.originalEvent = originalEvent;
  1004. // perform hit testing for events targeting our canvas or cancel events
  1005. this.processInteractive(interactionEvent, this.scene, func, cancelled || !eventAppend);
  1006. this.emit(cancelled ? 'pointercancel' : `pointerup${eventAppend}`, interactionEvent);
  1007. if (event.pointerType === 'mouse' || event.pointerType === 'pen') {
  1008. const isRightButton = event.button === 2;
  1009. this.emit(isRightButton ? `rightup${eventAppend}` : `mouseup${eventAppend}`, interactionEvent);
  1010. } else if (event.pointerType === 'touch') {
  1011. this.emit(cancelled ? 'touchcancel' : `touchend${eventAppend}`, interactionEvent);
  1012. this.releaseInteractionDataForPointerId(event.pointerId, interactionData);
  1013. }
  1014. }
  1015. }
  1016. /**
  1017. * Is called when the pointer button is cancelled
  1018. *
  1019. * @private
  1020. * @param {PointerEvent} event - The DOM event of a pointer button being released
  1021. */
  1022. onPointerCancel(event) {
  1023. // if we support touch events, then only use those for touch events, not pointer events
  1024. if (this.supportsTouchEvents && event.pointerType === 'touch') return;
  1025. this.onPointerComplete(event, true, this.processPointerCancel);
  1026. }
  1027. /**
  1028. * Processes the result of the pointer cancel check and dispatches the event if need be
  1029. *
  1030. * @private
  1031. * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event
  1032. * @param {Object3D} displayObject - The display object that was tested
  1033. */
  1034. processPointerCancel(interactionEvent, displayObject) {
  1035. const data = interactionEvent.data;
  1036. const id = interactionEvent.data.identifier;
  1037. if (displayObject.trackedPointers[id] !== undefined) {
  1038. delete displayObject.trackedPointers[id];
  1039. this.triggerEvent(displayObject, 'pointercancel', interactionEvent);
  1040. if (data.pointerType === 'touch') {
  1041. this.triggerEvent(displayObject, 'touchcancel', interactionEvent);
  1042. }
  1043. }
  1044. }
  1045. /**
  1046. * Is called when the pointer button is released on the renderer element
  1047. *
  1048. * @private
  1049. * @param {PointerEvent} event - The DOM event of a pointer button being released
  1050. */
  1051. onPointerUp(event) {
  1052. // if we support touch events, then only use those for touch events, not pointer events
  1053. if (this.supportsTouchEvents && event.pointerType === 'touch') return;
  1054. this.onPointerComplete(event, false, this.processPointerUp);
  1055. }
  1056. /**
  1057. * Processes the result of the pointer up check and dispatches the event if need be
  1058. *
  1059. * @private
  1060. * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event
  1061. * @param {Object3D} displayObject - The display object that was tested
  1062. * @param {boolean} hit - the result of the hit test on the display object
  1063. */
  1064. processPointerUp(interactionEvent, displayObject, hit) {
  1065. const data = interactionEvent.data;
  1066. const id = interactionEvent.data.identifier;
  1067. const trackingData = displayObject.trackedPointers[id];
  1068. const isTouch = data.pointerType === 'touch';
  1069. const isMouse = (data.pointerType === 'mouse' || data.pointerType === 'pen');
  1070. // Mouse only
  1071. if (isMouse) {
  1072. const isRightButton = data.button === 2;
  1073. const flags = InteractionTrackingData.FLAGS;
  1074. const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN;
  1075. const isDown = trackingData !== undefined && (trackingData.flags & test);
  1076. if (hit) {
  1077. this.triggerEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent);
  1078. if (isDown) {
  1079. this.triggerEvent(displayObject, isRightButton ? 'rightclick' : 'leftclick', interactionEvent);
  1080. }
  1081. } else if (isDown) {
  1082. this.triggerEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent);
  1083. }
  1084. // update the down state of the tracking data
  1085. if (trackingData) {
  1086. if (isRightButton) {
  1087. trackingData.rightDown = false;
  1088. } else {
  1089. trackingData.leftDown = false;
  1090. }
  1091. }
  1092. }
  1093. // Pointers and Touches, and Mouse
  1094. if (isTouch && displayObject.started) {
  1095. displayObject.started = false;
  1096. this.triggerEvent(displayObject, 'touchend', interactionEvent);
  1097. }
  1098. if (hit) {
  1099. this.triggerEvent(displayObject, 'pointerup', interactionEvent);
  1100. if (trackingData) {
  1101. this.triggerEvent(displayObject, 'pointertap', interactionEvent);
  1102. if (isTouch) {
  1103. this.triggerEvent(displayObject, 'tap', interactionEvent);
  1104. // touches are no longer over (if they ever were) when we get the touchend
  1105. // so we should ensure that we don't keep pretending that they are
  1106. trackingData.over = false;
  1107. }
  1108. }
  1109. } else if (trackingData) {
  1110. this.triggerEvent(displayObject, 'pointerupoutside', interactionEvent);
  1111. if (isTouch) this.triggerEvent(displayObject, 'touchendoutside', interactionEvent);
  1112. }
  1113. // Only remove the tracking data if there is no over/down state still associated with it
  1114. if (trackingData && trackingData.none) {
  1115. delete displayObject.trackedPointers[id];
  1116. }
  1117. }
  1118. /**
  1119. * Is called when the pointer moves across the renderer element
  1120. *
  1121. * @private
  1122. * @param {PointerEvent} originalEvent - The DOM event of a pointer moving
  1123. */
  1124. onPointerMove(originalEvent) {
  1125. // if we support touch events, then only use those for touch events, not pointer events
  1126. if (this.supportsTouchEvents && originalEvent.pointerType === 'touch') return;
  1127. const events = this.normalizeToPointerData(originalEvent);
  1128. if (events[0].pointerType === 'mouse') {
  1129. this.didMove = true;
  1130. this.cursor = null;
  1131. }
  1132. const eventLen = events.length;
  1133. for (let i = 0; i < eventLen; i++) {
  1134. const event = events[i];
  1135. const interactionData = this.getInteractionDataForPointerId(event);
  1136. const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData);
  1137. interactionEvent.data.originalEvent = originalEvent;
  1138. const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true;
  1139. this.processInteractive(
  1140. interactionEvent,
  1141. this.scene,
  1142. this.processPointerMove,
  1143. interactive
  1144. );
  1145. this.emit('pointermove', interactionEvent);
  1146. if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent);
  1147. if (event.pointerType === 'mouse' || event.pointerType === 'pen') this.emit('mousemove', interactionEvent);
  1148. }
  1149. if (events[0].pointerType === 'mouse') {
  1150. this.setCursorMode(this.cursor);
  1151. // TODO BUG for parents interactive object (border order issue)
  1152. }
  1153. }
  1154. /**
  1155. * Processes the result of the pointer move check and dispatches the event if need be
  1156. *
  1157. * @private
  1158. * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event
  1159. * @param {Object3D} displayObject - The display object that was tested
  1160. * @param {boolean} hit - the result of the hit test on the display object
  1161. */
  1162. processPointerMove(interactionEvent, displayObject, hit) {
  1163. const data = interactionEvent.data;
  1164. const isTouch = data.pointerType === 'touch';
  1165. const isMouse = (data.pointerType === 'mouse' || data.pointerType === 'pen');
  1166. if (isMouse) {
  1167. this.processPointerOverOut(interactionEvent, displayObject, hit);
  1168. }
  1169. if (isTouch && displayObject.started) this.triggerEvent(displayObject, 'touchmove', interactionEvent);
  1170. if (!this.moveWhenInside || hit) {
  1171. this.triggerEvent(displayObject, 'pointermove', interactionEvent);
  1172. if (isMouse) this.triggerEvent(displayObject, 'mousemove', interactionEvent);
  1173. }
  1174. }
  1175. /**
  1176. * Is called when the pointer is moved out of the renderer element
  1177. *
  1178. * @private
  1179. * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out
  1180. */
  1181. onPointerOut(originalEvent) {
  1182. // if we support touch events, then only use those for touch events, not pointer events
  1183. if (this.supportsTouchEvents && originalEvent.pointerType === 'touch') return;
  1184. const events = this.normalizeToPointerData(originalEvent);
  1185. // Only mouse and pointer can call onPointerOut, so events will always be length 1
  1186. const event = events[0];
  1187. if (event.pointerType === 'mouse') {
  1188. this.mouseOverRenderer = false;
  1189. this.setCursorMode(null);
  1190. }
  1191. const interactionData = this.getInteractionDataForPointerId(event);
  1192. const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData);
  1193. interactionEvent.data.originalEvent = event;
  1194. this.processInteractive(interactionEvent, this.scene, this.processPointerOverOut, false);
  1195. this.emit('pointerout', interactionEvent);
  1196. if (event.pointerType === 'mouse' || event.pointerType === 'pen') {
  1197. this.emit('mouseout', interactionEvent);
  1198. } else {
  1199. // we can get touchleave events after touchend, so we want to make sure we don't
  1200. // introduce memory leaks
  1201. this.releaseInteractionDataForPointerId(interactionData.identifier);
  1202. }
  1203. }
  1204. /**
  1205. * Processes the result of the pointer over/out check and dispatches the event if need be
  1206. *
  1207. * @private
  1208. * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event
  1209. * @param {Object3D} displayObject - The display object that was tested
  1210. * @param {boolean} hit - the result of the hit test on the display object
  1211. */
  1212. processPointerOverOut(interactionEvent, displayObject, hit) {
  1213. const data = interactionEvent.data;
  1214. const id = interactionEvent.data.identifier;
  1215. const isMouse = (data.pointerType === 'mouse' || data.pointerType === 'pen');
  1216. let trackingData = displayObject.trackedPointers[id];
  1217. // if we just moused over the display object, then we need to track that state
  1218. if (hit && !trackingData) {
  1219. trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(id);
  1220. }
  1221. if (trackingData === undefined) return;
  1222. if (hit && this.mouseOverRenderer) {
  1223. if (!trackingData.over) {
  1224. trackingData.over = true;
  1225. this.triggerEvent(displayObject, 'pointerover', interactionEvent);
  1226. if (isMouse) {
  1227. this.triggerEvent(displayObject, 'mouseover', interactionEvent);
  1228. }
  1229. }
  1230. // only change the cursor if it has not already been changed (by something deeper in the
  1231. // display tree)
  1232. if (isMouse && this.cursor === null) {
  1233. this.cursor = displayObject.cursor;
  1234. }
  1235. } else if (trackingData.over) {
  1236. trackingData.over = false;
  1237. this.triggerEvent(displayObject, 'pointerout', this.eventData);
  1238. if (isMouse) {
  1239. this.triggerEvent(displayObject, 'mouseout', interactionEvent);
  1240. }
  1241. // if there is no mouse down information for the pointer, then it is safe to delete
  1242. if (trackingData.none) {
  1243. delete displayObject.trackedPointers[id];
  1244. }
  1245. }
  1246. }
  1247. /**
  1248. * Is called when the pointer is moved into the renderer element
  1249. *
  1250. * @private
  1251. * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view
  1252. */
  1253. onPointerOver(originalEvent) {
  1254. const events = this.normalizeToPointerData(originalEvent);
  1255. // Only mouse and pointer can call onPointerOver, so events will always be length 1
  1256. const event = events[0];
  1257. const interactionData = this.getInteractionDataForPointerId(event);
  1258. const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData);
  1259. interactionEvent.data.originalEvent = event;
  1260. if (event.pointerType === 'mouse') {
  1261. this.mouseOverRenderer = true;
  1262. }
  1263. this.emit('pointerover', interactionEvent);
  1264. if (event.pointerType === 'mouse' || event.pointerType === 'pen') {
  1265. this.emit('mouseover', interactionEvent);
  1266. }
  1267. }
  1268. /**
  1269. * Get InteractionData for a given pointerId. Store that data as well
  1270. *
  1271. * @private
  1272. * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData
  1273. * @return {InteractionData} - Interaction data for the given pointer identifier
  1274. */
  1275. getInteractionDataForPointerId(event) {
  1276. const pointerId = event.pointerId;
  1277. let interactionData;
  1278. if (pointerId === MOUSE_POINTER_ID || event.pointerType === 'mouse') {
  1279. interactionData = this.mouse;
  1280. } else if (this.activeInteractionData[pointerId]) {
  1281. interactionData = this.activeInteractionData[pointerId];
  1282. } else {
  1283. interactionData = this.interactionDataPool.pop() || new InteractionData();
  1284. interactionData.identifier = pointerId;
  1285. this.activeInteractionData[pointerId] = interactionData;
  1286. }
  1287. // copy properties from the event, so that we can make sure that touch/pointer specific
  1288. // data is available
  1289. interactionData._copyEvent(event);
  1290. return interactionData;
  1291. }
  1292. /**
  1293. * Return unused InteractionData to the pool, for a given pointerId
  1294. *
  1295. * @private
  1296. * @param {number} pointerId - Identifier from a pointer event
  1297. */
  1298. releaseInteractionDataForPointerId(pointerId) {
  1299. const interactionData = this.activeInteractionData[pointerId];
  1300. if (interactionData) {
  1301. delete this.activeInteractionData[pointerId];
  1302. interactionData._reset();
  1303. this.interactionDataPool.push(interactionData);
  1304. }
  1305. }
  1306. /**
  1307. * Maps x and y coords from a DOM object and maps them correctly to the three.js view. The
  1308. * resulting value is stored in the point. This takes into account the fact that the DOM
  1309. * element could be scaled and positioned anywhere on the screen.
  1310. *
  1311. * @param {Vector2} point - the point that the result will be stored in
  1312. * @param {number} x - the x coord of the position to map
  1313. * @param {number} y - the y coord of the position to map
  1314. */
  1315. mapPositionToPoint(point, x, y) {
  1316. let rect;
  1317. // IE 11 fix
  1318. if (!this.interactionDOMElement.parentElement) {
  1319. rect = {
  1320. x: 0,
  1321. y: 0,
  1322. left: 0,
  1323. top: 0,
  1324. width: 0,
  1325. height: 0,
  1326. };
  1327. } else {
  1328. rect = this.interactionDOMElement.getBoundingClientRect();
  1329. }
  1330. point.x = ((x - rect.left) / rect.width) * 2 - 1;
  1331. point.y = -((y - rect.top) / rect.height) * 2 + 1;
  1332. }
  1333. /**
  1334. * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData
  1335. *
  1336. * @private
  1337. * @param {InteractionEvent} interactionEvent - The event to be configured
  1338. * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent
  1339. * @param {InteractionData} interactionData - The InteractionData that will be paired
  1340. * with the InteractionEvent
  1341. * @return {InteractionEvent} the interaction event that was passed in
  1342. */
  1343. configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) {
  1344. interactionEvent.data = interactionData;
  1345. this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY);
  1346. this.raycaster.setFromCamera(interactionData.global, this.camera);
  1347. // Not really sure why this is happening, but it's how a previous version handled things TODO: there should be remove
  1348. if (pointerEvent.pointerType === 'touch') {
  1349. pointerEvent.globalX = interactionData.global.x;
  1350. pointerEvent.globalY = interactionData.global.y;
  1351. }
  1352. interactionData.originalEvent = pointerEvent;
  1353. interactionEvent._reset();
  1354. interactionEvent.intersects = this.raycaster.intersectObjects(this.scene.children, true);
  1355. return interactionEvent;
  1356. }
  1357. /**
  1358. * Ensures that the original event object contains all data that a regular pointer event would have
  1359. *
  1360. * @private
  1361. * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event
  1362. * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer
  1363. * or mouse event, or a multiple normalized pointer events if there are multiple changed touches
  1364. */
  1365. normalizeToPointerData(event) {
  1366. const normalizedEvents = [];
  1367. if (this.supportsTouchEvents && event instanceof TouchEvent) {
  1368. for (let i = 0, li = event.changedTouches.length; i < li; i++) {
  1369. const touch = event.changedTouches[i];
  1370. if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0;
  1371. if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0;
  1372. if (typeof touch.isPrimary === 'undefined') {
  1373. touch.isPrimary = event.touches.length === 1 && event.type === 'touchstart';
  1374. }
  1375. if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1;
  1376. if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1;
  1377. if (typeof touch.tiltX === 'undefined') touch.tiltX = 0;
  1378. if (typeof touch.tiltY === 'undefined') touch.tiltY = 0;
  1379. if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch';
  1380. if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0;
  1381. if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5;
  1382. touch.twist = 0;
  1383. touch.tangentialPressure = 0;
  1384. // TODO: Remove these, as layerX/Y is not a standard, is deprecated, has uneven
  1385. // support, and the fill ins are not quite the same
  1386. // offsetX/Y might be okay, but is not the same as clientX/Y when the canvas's top
  1387. // left is not 0,0 on the page
  1388. if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX;
  1389. if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY;
  1390. // mark the touch as normalized, just so that we know we did it
  1391. touch.isNormalized = true;
  1392. normalizedEvents.push(touch);
  1393. }
  1394. } else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) {
  1395. if (typeof event.isPrimary === 'undefined') event.isPrimary = true;
  1396. if (typeof event.width === 'undefined') event.width = 1;
  1397. if (typeof event.height === 'undefined') event.height = 1;
  1398. if (typeof event.tiltX === 'undefined') event.tiltX = 0;
  1399. if (typeof event.tiltY === 'undefined') event.tiltY = 0;
  1400. if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse';
  1401. if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID;
  1402. if (typeof event.pressure === 'undefined') event.pressure = 0.5;
  1403. event.twist = 0;
  1404. event.tangentialPressure = 0;
  1405. // mark the mouse event as normalized, just so that we know we did it
  1406. event.isNormalized = true;
  1407. normalizedEvents.push(event);
  1408. } else {
  1409. normalizedEvents.push(event);
  1410. }
  1411. return normalizedEvents;
  1412. }
  1413. /**
  1414. * Destroys the interaction manager
  1415. *
  1416. */
  1417. destroy() {
  1418. this.removeEvents();
  1419. this.removeAllListeners();
  1420. this.renderer = null;
  1421. this.mouse = null;
  1422. this.eventData = null;
  1423. this.interactionDOMElement = null;
  1424. this.onPointerDown = null;
  1425. this.processPointerDown = null;
  1426. this.onPointerUp = null;
  1427. this.processPointerUp = null;
  1428. this.onPointerCancel = null;
  1429. this.processPointerCancel = null;
  1430. this.onPointerMove = null;
  1431. this.processPointerMove = null;
  1432. this.onPointerOut = null;
  1433. this.processPointerOverOut = null;
  1434. this.onPointerOver = null;
  1435. this._tempPoint = null;
  1436. }
  1437. }
  1438. export default InteractionManager;