scene.inputManager.ts 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. import { Observable, Observer } from "../Misc/observable";
  2. import { PointerInfoPre, PointerInfo, PointerEventTypes } from "../Events/pointerEvents";
  3. import { Nullable } from "../types";
  4. import { AbstractActionManager } from "../Actions/abstractActionManager";
  5. import { PickingInfo } from "../Collisions/pickingInfo";
  6. import { Vector2, Matrix } from "../Maths/math.vector";
  7. import { AbstractMesh } from "../Meshes/abstractMesh";
  8. import { Constants } from "../Engines/constants";
  9. import { ActionEvent } from "../Actions/actionEvent";
  10. import { Tools } from "../Misc/tools";
  11. import { Engine } from "../Engines/engine";
  12. import { KeyboardEventTypes, KeyboardInfoPre, KeyboardInfo } from "../Events/keyboardEvents";
  13. declare type Scene = import("../scene").Scene;
  14. /** @hidden */
  15. class _ClickInfo {
  16. private _singleClick = false;
  17. private _doubleClick = false;
  18. private _hasSwiped = false;
  19. private _ignore = false;
  20. public get singleClick(): boolean {
  21. return this._singleClick;
  22. }
  23. public get doubleClick(): boolean {
  24. return this._doubleClick;
  25. }
  26. public get hasSwiped(): boolean {
  27. return this._hasSwiped;
  28. }
  29. public get ignore(): boolean {
  30. return this._ignore;
  31. }
  32. public set singleClick(b: boolean) {
  33. this._singleClick = b;
  34. }
  35. public set doubleClick(b: boolean) {
  36. this._doubleClick = b;
  37. }
  38. public set hasSwiped(b: boolean) {
  39. this._hasSwiped = b;
  40. }
  41. public set ignore(b: boolean) {
  42. this._ignore = b;
  43. }
  44. }
  45. /**
  46. * Class used to manage all inputs for the scene.
  47. */
  48. export class InputManager {
  49. /** The distance in pixel that you have to move to prevent some events */
  50. public static DragMovementThreshold = 10; // in pixels
  51. /** Time in milliseconds to wait to raise long press events if button is still pressed */
  52. public static LongPressDelay = 500; // in milliseconds
  53. /** Time in milliseconds with two consecutive clicks will be considered as a double click */
  54. public static DoubleClickDelay = 300; // in milliseconds
  55. /** If you need to check double click without raising a single click at first click, enable this flag */
  56. public static ExclusiveDoubleClickMode = false;
  57. /** This is a defensive check to not allow control attachment prior to an already active one. If already attached, previous control is unattached before attaching the new one. */
  58. private _alreadyAttached = false;
  59. // Pointers
  60. private _wheelEventName = "";
  61. private _onPointerMove: (evt: PointerEvent) => void;
  62. private _onPointerDown: (evt: PointerEvent) => void;
  63. private _onPointerUp: (evt: PointerEvent) => void;
  64. private _initClickEvent: (obs1: Observable<PointerInfoPre>, obs2: Observable<PointerInfo>, evt: PointerEvent, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void) => void;
  65. private _initActionManager: (act: Nullable<AbstractActionManager>, clickInfo: _ClickInfo) => Nullable<AbstractActionManager>;
  66. private _delayedSimpleClick: (btn: number, clickInfo: _ClickInfo, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void) => void;
  67. private _delayedSimpleClickTimeout: number;
  68. private _previousDelayedSimpleClickTimeout: number;
  69. private _meshPickProceed = false;
  70. private _previousButtonPressed: number;
  71. private _currentPickResult: Nullable<PickingInfo> = null;
  72. private _previousPickResult: Nullable<PickingInfo> = null;
  73. private _totalPointersPressed = 0;
  74. private _doubleClickOccured = false;
  75. private _pointerOverMesh: Nullable<AbstractMesh>;
  76. private _pickedDownMesh: Nullable<AbstractMesh>;
  77. private _pickedUpMesh: Nullable<AbstractMesh>;
  78. private _pointerX: number = 0;
  79. private _pointerY: number = 0;
  80. private _unTranslatedPointerX: number;
  81. private _unTranslatedPointerY: number;
  82. private _startingPointerPosition = new Vector2(0, 0);
  83. private _previousStartingPointerPosition = new Vector2(0, 0);
  84. private _startingPointerTime = 0;
  85. private _previousStartingPointerTime = 0;
  86. private _pointerCaptures: { [pointerId: number]: boolean } = {};
  87. private _meshUnderPointerId: Nullable<AbstractMesh>[] = [];
  88. // Keyboard
  89. private _onKeyDown: (evt: KeyboardEvent) => void;
  90. private _onKeyUp: (evt: KeyboardEvent) => void;
  91. private _onCanvasFocusObserver: Nullable<Observer<Engine>>;
  92. private _onCanvasBlurObserver: Nullable<Observer<Engine>>;
  93. private _scene: Scene;
  94. /**
  95. * Creates a new InputManager
  96. * @param scene defines the hosting scene
  97. */
  98. public constructor(scene: Scene) {
  99. this._scene = scene;
  100. }
  101. /**
  102. * Gets the mesh that is currently under the pointer
  103. */
  104. public get meshUnderPointer(): Nullable<AbstractMesh> {
  105. return this._pointerOverMesh;
  106. }
  107. /**
  108. * When using more than one pointer (for example in XR) you can get the mesh under the specific pointer
  109. * @param pointerId the pointer id to use
  110. * @returns The mesh under this pointer id or null if not found
  111. */
  112. public getMeshUnderPointerByPointerId(pointerId: number): Nullable<AbstractMesh> {
  113. return this._meshUnderPointerId[pointerId];
  114. }
  115. /**
  116. * Gets the pointer coordinates in 2D without any translation (ie. straight out of the pointer event)
  117. */
  118. public get unTranslatedPointer(): Vector2 {
  119. return new Vector2(this._unTranslatedPointerX, this._unTranslatedPointerY);
  120. }
  121. /**
  122. * Gets or sets the current on-screen X position of the pointer
  123. */
  124. public get pointerX(): number {
  125. return this._pointerX;
  126. }
  127. public set pointerX(value: number) {
  128. this._pointerX = value;
  129. }
  130. /**
  131. * Gets or sets the current on-screen Y position of the pointer
  132. */
  133. public get pointerY(): number {
  134. return this._pointerY;
  135. }
  136. public set pointerY(value: number) {
  137. this._pointerY = value;
  138. }
  139. private _updatePointerPosition(evt: PointerEvent): void {
  140. var canvasRect = this._scene.getEngine().getInputElementClientRect();
  141. if (!canvasRect) {
  142. return;
  143. }
  144. this._pointerX = evt.clientX - canvasRect.left;
  145. this._pointerY = evt.clientY - canvasRect.top;
  146. this._unTranslatedPointerX = this._pointerX;
  147. this._unTranslatedPointerY = this._pointerY;
  148. }
  149. private _processPointerMove(pickResult: Nullable<PickingInfo>, evt: PointerEvent) {
  150. let scene = this._scene;
  151. let engine = scene.getEngine();
  152. var canvas = engine.getInputElement();
  153. if (!canvas) {
  154. return;
  155. }
  156. canvas.tabIndex = engine.canvasTabIndex;
  157. // Restore pointer
  158. if (!scene.doNotHandleCursors) {
  159. canvas.style.cursor = scene.defaultCursor;
  160. }
  161. var isMeshPicked = pickResult && pickResult.hit && pickResult.pickedMesh ? true : false;
  162. if (isMeshPicked) {
  163. scene.setPointerOverMesh(pickResult!.pickedMesh, evt.pointerId);
  164. if (this._pointerOverMesh && this._pointerOverMesh.actionManager && this._pointerOverMesh.actionManager.hasPointerTriggers) {
  165. if (!scene.doNotHandleCursors) {
  166. if (this._pointerOverMesh.actionManager.hoverCursor) {
  167. canvas.style.cursor = this._pointerOverMesh.actionManager.hoverCursor;
  168. } else {
  169. canvas.style.cursor = scene.hoverCursor;
  170. }
  171. }
  172. }
  173. } else {
  174. scene.setPointerOverMesh(null, evt.pointerId);
  175. }
  176. for (let step of scene._pointerMoveStage) {
  177. pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, isMeshPicked, canvas);
  178. }
  179. if (pickResult) {
  180. let type = evt.type === this._wheelEventName ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE;
  181. if (scene.onPointerMove) {
  182. scene.onPointerMove(evt, pickResult, type);
  183. }
  184. if (scene.onPointerObservable.hasObservers()) {
  185. let pi = new PointerInfo(type, evt, pickResult);
  186. this._setRayOnPointerInfo(pi);
  187. scene.onPointerObservable.notifyObservers(pi, type);
  188. }
  189. }
  190. }
  191. // Pointers handling
  192. private _setRayOnPointerInfo(pointerInfo: PointerInfo) {
  193. let scene = this._scene;
  194. if (pointerInfo.pickInfo && !pointerInfo.pickInfo._pickingUnavailable) {
  195. if (!pointerInfo.pickInfo.ray) {
  196. pointerInfo.pickInfo.ray = scene.createPickingRay(pointerInfo.event.offsetX, pointerInfo.event.offsetY, Matrix.Identity(), scene.activeCamera);
  197. }
  198. }
  199. }
  200. private _checkPrePointerObservable(pickResult: Nullable<PickingInfo>, evt: PointerEvent, type: number) {
  201. let scene = this._scene;
  202. let pi = new PointerInfoPre(type, evt, this._unTranslatedPointerX, this._unTranslatedPointerY);
  203. if (pickResult) {
  204. pi.ray = pickResult.ray;
  205. }
  206. scene.onPrePointerObservable.notifyObservers(pi, type);
  207. if (pi.skipOnPointerObservable) {
  208. return true;
  209. } else {
  210. return false;
  211. }
  212. }
  213. /**
  214. * Use this method to simulate a pointer move on a mesh
  215. * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
  216. * @param pickResult pickingInfo of the object wished to simulate pointer event on
  217. * @param pointerEventInit pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
  218. */
  219. public simulatePointerMove(pickResult: PickingInfo, pointerEventInit?: PointerEventInit): void {
  220. let evt = new PointerEvent("pointermove", pointerEventInit);
  221. if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERMOVE)) {
  222. return;
  223. }
  224. this._processPointerMove(pickResult, evt);
  225. }
  226. /**
  227. * Use this method to simulate a pointer down on a mesh
  228. * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
  229. * @param pickResult pickingInfo of the object wished to simulate pointer event on
  230. * @param pointerEventInit pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
  231. */
  232. public simulatePointerDown(pickResult: PickingInfo, pointerEventInit?: PointerEventInit): void {
  233. let evt = new PointerEvent("pointerdown", pointerEventInit);
  234. if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERDOWN)) {
  235. return;
  236. }
  237. this._processPointerDown(pickResult, evt);
  238. }
  239. private _processPointerDown(pickResult: Nullable<PickingInfo>, evt: PointerEvent): void {
  240. let scene = this._scene;
  241. if (pickResult && pickResult.hit && pickResult.pickedMesh) {
  242. this._pickedDownMesh = pickResult.pickedMesh;
  243. var actionManager = pickResult.pickedMesh._getActionManagerForTrigger();
  244. if (actionManager) {
  245. if (actionManager.hasPickTriggers) {
  246. actionManager.processTrigger(Constants.ACTION_OnPickDownTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  247. switch (evt.button) {
  248. case 0:
  249. actionManager.processTrigger(Constants.ACTION_OnLeftPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  250. break;
  251. case 1:
  252. actionManager.processTrigger(Constants.ACTION_OnCenterPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  253. break;
  254. case 2:
  255. actionManager.processTrigger(Constants.ACTION_OnRightPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  256. break;
  257. }
  258. }
  259. if (actionManager.hasSpecificTrigger(Constants.ACTION_OnLongPressTrigger)) {
  260. window.setTimeout(() => {
  261. var pickResult = scene.pick(
  262. this._unTranslatedPointerX,
  263. this._unTranslatedPointerY,
  264. (mesh: AbstractMesh): boolean => <boolean>(mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.actionManager && mesh.actionManager.hasSpecificTrigger(Constants.ACTION_OnLongPressTrigger) && mesh == this._pickedDownMesh),
  265. false,
  266. scene.cameraToUseForPointers
  267. );
  268. if (pickResult && pickResult.hit && pickResult.pickedMesh && actionManager) {
  269. if (this._totalPointersPressed !== 0 && Date.now() - this._startingPointerTime > InputManager.LongPressDelay && !this._isPointerSwiping()) {
  270. this._startingPointerTime = 0;
  271. actionManager.processTrigger(Constants.ACTION_OnLongPressTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  272. }
  273. }
  274. }, InputManager.LongPressDelay);
  275. }
  276. }
  277. } else {
  278. for (let step of scene._pointerDownStage) {
  279. pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt);
  280. }
  281. }
  282. if (pickResult) {
  283. let type = PointerEventTypes.POINTERDOWN;
  284. if (scene.onPointerDown) {
  285. scene.onPointerDown(evt, pickResult, type);
  286. }
  287. if (scene.onPointerObservable.hasObservers()) {
  288. let pi = new PointerInfo(type, evt, pickResult);
  289. this._setRayOnPointerInfo(pi);
  290. scene.onPointerObservable.notifyObservers(pi, type);
  291. }
  292. }
  293. }
  294. /** @hidden */
  295. public _isPointerSwiping(): boolean {
  296. return Math.abs(this._startingPointerPosition.x - this._pointerX) > InputManager.DragMovementThreshold || Math.abs(this._startingPointerPosition.y - this._pointerY) > InputManager.DragMovementThreshold;
  297. }
  298. /**
  299. * Use this method to simulate a pointer up on a mesh
  300. * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
  301. * @param pickResult pickingInfo of the object wished to simulate pointer event on
  302. * @param pointerEventInit pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
  303. * @param doubleTap indicates that the pointer up event should be considered as part of a double click (false by default)
  304. */
  305. public simulatePointerUp(pickResult: PickingInfo, pointerEventInit?: PointerEventInit, doubleTap?: boolean): void {
  306. let evt = new PointerEvent("pointerup", pointerEventInit);
  307. let clickInfo = new _ClickInfo();
  308. if (doubleTap) {
  309. clickInfo.doubleClick = true;
  310. } else {
  311. clickInfo.singleClick = true;
  312. }
  313. if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERUP)) {
  314. return;
  315. }
  316. this._processPointerUp(pickResult, evt, clickInfo);
  317. }
  318. private _processPointerUp(pickResult: Nullable<PickingInfo>, evt: PointerEvent, clickInfo: _ClickInfo): void {
  319. let scene = this._scene;
  320. if (pickResult && pickResult && pickResult.pickedMesh) {
  321. this._pickedUpMesh = pickResult.pickedMesh;
  322. if (this._pickedDownMesh === this._pickedUpMesh) {
  323. if (scene.onPointerPick) {
  324. scene.onPointerPick(evt, pickResult);
  325. }
  326. if (clickInfo.singleClick && !clickInfo.ignore && scene.onPointerObservable.hasObservers()) {
  327. let type = PointerEventTypes.POINTERPICK;
  328. let pi = new PointerInfo(type, evt, pickResult);
  329. this._setRayOnPointerInfo(pi);
  330. scene.onPointerObservable.notifyObservers(pi, type);
  331. }
  332. }
  333. let actionManager = pickResult.pickedMesh._getActionManagerForTrigger();
  334. if (actionManager && !clickInfo.ignore) {
  335. actionManager.processTrigger(Constants.ACTION_OnPickUpTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  336. if (!clickInfo.hasSwiped && clickInfo.singleClick) {
  337. actionManager.processTrigger(Constants.ACTION_OnPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  338. }
  339. let doubleClickActionManager = pickResult.pickedMesh._getActionManagerForTrigger(Constants.ACTION_OnDoublePickTrigger);
  340. if (clickInfo.doubleClick && doubleClickActionManager) {
  341. doubleClickActionManager.processTrigger(Constants.ACTION_OnDoublePickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  342. }
  343. }
  344. } else {
  345. if (!clickInfo.ignore) {
  346. for (let step of scene._pointerUpStage) {
  347. pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt);
  348. }
  349. }
  350. }
  351. if (this._pickedDownMesh && this._pickedDownMesh !== this._pickedUpMesh) {
  352. let pickedDownActionManager = this._pickedDownMesh._getActionManagerForTrigger(Constants.ACTION_OnPickOutTrigger);
  353. if (pickedDownActionManager) {
  354. pickedDownActionManager.processTrigger(Constants.ACTION_OnPickOutTrigger, ActionEvent.CreateNew(this._pickedDownMesh, evt));
  355. }
  356. }
  357. let type = 0;
  358. if (scene.onPointerObservable.hasObservers()) {
  359. if (!clickInfo.ignore && !clickInfo.hasSwiped) {
  360. if (clickInfo.singleClick && scene.onPointerObservable.hasSpecificMask(PointerEventTypes.POINTERTAP)) {
  361. type = PointerEventTypes.POINTERTAP;
  362. } else if (clickInfo.doubleClick && scene.onPointerObservable.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP)) {
  363. type = PointerEventTypes.POINTERDOUBLETAP;
  364. }
  365. if (type) {
  366. let pi = new PointerInfo(type, evt, pickResult);
  367. this._setRayOnPointerInfo(pi);
  368. scene.onPointerObservable.notifyObservers(pi, type);
  369. }
  370. }
  371. if (!clickInfo.ignore) {
  372. type = PointerEventTypes.POINTERUP;
  373. let pi = new PointerInfo(type, evt, pickResult);
  374. this._setRayOnPointerInfo(pi);
  375. scene.onPointerObservable.notifyObservers(pi, type);
  376. }
  377. }
  378. if (scene.onPointerUp && !clickInfo.ignore) {
  379. scene.onPointerUp(evt, pickResult, type);
  380. }
  381. }
  382. /**
  383. * Gets a boolean indicating if the current pointer event is captured (meaning that the scene has already handled the pointer down)
  384. * @param pointerId defines the pointer id to use in a multi-touch scenario (0 by default)
  385. * @returns true if the pointer was captured
  386. */
  387. public isPointerCaptured(pointerId = 0): boolean {
  388. return this._pointerCaptures[pointerId];
  389. }
  390. /**
  391. * Attach events to the canvas (To handle actionManagers triggers and raise onPointerMove, onPointerDown and onPointerUp
  392. * @param attachUp defines if you want to attach events to pointerup
  393. * @param attachDown defines if you want to attach events to pointerdown
  394. * @param attachMove defines if you want to attach events to pointermove
  395. * @param elementToAttachTo defines the target DOM element to attach to (will use the canvas by default)
  396. */
  397. public attachControl(attachUp = true, attachDown = true, attachMove = true, elementToAttachTo: Nullable<HTMLElement> = null): void {
  398. let scene = this._scene;
  399. if (!elementToAttachTo) {
  400. elementToAttachTo = scene.getEngine().getInputElement();
  401. }
  402. if (!elementToAttachTo) {
  403. return;
  404. }
  405. if (this._alreadyAttached) {
  406. this.detachControl();
  407. }
  408. let engine = scene.getEngine();
  409. this._initActionManager = (act: Nullable<AbstractActionManager>, clickInfo: _ClickInfo): Nullable<AbstractActionManager> => {
  410. if (!this._meshPickProceed) {
  411. let pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerDownPredicate, false, scene.cameraToUseForPointers);
  412. this._currentPickResult = pickResult;
  413. if (pickResult) {
  414. act = pickResult.hit && pickResult.pickedMesh ? pickResult.pickedMesh._getActionManagerForTrigger() : null;
  415. }
  416. this._meshPickProceed = true;
  417. }
  418. return act;
  419. };
  420. this._delayedSimpleClick = (btn: number, clickInfo: _ClickInfo, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void) => {
  421. // double click delay is over and that no double click has been raised since, or the 2 consecutive keys pressed are different
  422. if ((Date.now() - this._previousStartingPointerTime > InputManager.DoubleClickDelay && !this._doubleClickOccured) || btn !== this._previousButtonPressed) {
  423. this._doubleClickOccured = false;
  424. clickInfo.singleClick = true;
  425. clickInfo.ignore = false;
  426. cb(clickInfo, this._currentPickResult);
  427. }
  428. };
  429. this._initClickEvent = (obs1: Observable<PointerInfoPre>, obs2: Observable<PointerInfo>, evt: PointerEvent, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void): void => {
  430. let clickInfo = new _ClickInfo();
  431. this._currentPickResult = null;
  432. let act: Nullable<AbstractActionManager> = null;
  433. let checkPicking =
  434. obs1.hasSpecificMask(PointerEventTypes.POINTERPICK) ||
  435. obs2.hasSpecificMask(PointerEventTypes.POINTERPICK) ||
  436. obs1.hasSpecificMask(PointerEventTypes.POINTERTAP) ||
  437. obs2.hasSpecificMask(PointerEventTypes.POINTERTAP) ||
  438. obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) ||
  439. obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
  440. if (!checkPicking && AbstractActionManager) {
  441. act = this._initActionManager(act, clickInfo);
  442. if (act) {
  443. checkPicking = act.hasPickTriggers;
  444. }
  445. }
  446. let needToIgnoreNext = false;
  447. if (checkPicking) {
  448. let btn = evt.button;
  449. clickInfo.hasSwiped = this._isPointerSwiping();
  450. if (!clickInfo.hasSwiped) {
  451. let checkSingleClickImmediately = !InputManager.ExclusiveDoubleClickMode;
  452. if (!checkSingleClickImmediately) {
  453. checkSingleClickImmediately = !obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) && !obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
  454. if (checkSingleClickImmediately && !AbstractActionManager.HasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger)) {
  455. act = this._initActionManager(act, clickInfo);
  456. if (act) {
  457. checkSingleClickImmediately = !act.hasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger);
  458. }
  459. }
  460. }
  461. if (checkSingleClickImmediately) {
  462. // single click detected if double click delay is over or two different successive keys pressed without exclusive double click or no double click required
  463. if (Date.now() - this._previousStartingPointerTime > InputManager.DoubleClickDelay || btn !== this._previousButtonPressed) {
  464. clickInfo.singleClick = true;
  465. cb(clickInfo, this._currentPickResult);
  466. needToIgnoreNext = true;
  467. }
  468. }
  469. // at least one double click is required to be check and exclusive double click is enabled
  470. else {
  471. // wait that no double click has been raised during the double click delay
  472. this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
  473. this._delayedSimpleClickTimeout = window.setTimeout(this._delayedSimpleClick.bind(this, btn, clickInfo, cb), InputManager.DoubleClickDelay);
  474. }
  475. let checkDoubleClick = obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) || obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
  476. if (!checkDoubleClick && AbstractActionManager.HasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger)) {
  477. act = this._initActionManager(act, clickInfo);
  478. if (act) {
  479. checkDoubleClick = act.hasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger);
  480. }
  481. }
  482. if (checkDoubleClick) {
  483. // two successive keys pressed are equal, double click delay is not over and double click has not just occurred
  484. if (btn === this._previousButtonPressed && Date.now() - this._previousStartingPointerTime < InputManager.DoubleClickDelay && !this._doubleClickOccured) {
  485. // pointer has not moved for 2 clicks, it's a double click
  486. if (!clickInfo.hasSwiped && !this._isPointerSwiping()) {
  487. this._previousStartingPointerTime = 0;
  488. this._doubleClickOccured = true;
  489. clickInfo.doubleClick = true;
  490. clickInfo.ignore = false;
  491. if (InputManager.ExclusiveDoubleClickMode && this._previousDelayedSimpleClickTimeout) {
  492. clearTimeout(this._previousDelayedSimpleClickTimeout);
  493. }
  494. this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
  495. cb(clickInfo, this._currentPickResult);
  496. }
  497. // if the two successive clicks are too far, it's just two simple clicks
  498. else {
  499. this._doubleClickOccured = false;
  500. this._previousStartingPointerTime = this._startingPointerTime;
  501. this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
  502. this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
  503. this._previousButtonPressed = btn;
  504. if (InputManager.ExclusiveDoubleClickMode) {
  505. if (this._previousDelayedSimpleClickTimeout) {
  506. clearTimeout(this._previousDelayedSimpleClickTimeout);
  507. }
  508. this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
  509. cb(clickInfo, this._previousPickResult);
  510. } else {
  511. cb(clickInfo, this._currentPickResult);
  512. }
  513. }
  514. needToIgnoreNext = true;
  515. }
  516. // just the first click of the double has been raised
  517. else {
  518. this._doubleClickOccured = false;
  519. this._previousStartingPointerTime = this._startingPointerTime;
  520. this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
  521. this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
  522. this._previousButtonPressed = btn;
  523. }
  524. }
  525. }
  526. }
  527. if (!needToIgnoreNext) {
  528. cb(clickInfo, this._currentPickResult);
  529. }
  530. };
  531. this._onPointerMove = (evt: PointerEvent) => {
  532. // preserve compatibility with Safari when pointerId is not present
  533. if (evt.pointerId === undefined) {
  534. (evt as any).pointerId = 0;
  535. }
  536. this._updatePointerPosition(evt);
  537. // PreObservable support
  538. if (this._checkPrePointerObservable(null, evt, evt.type === this._wheelEventName ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE)) {
  539. return;
  540. }
  541. if (!scene.cameraToUseForPointers && !scene.activeCamera) {
  542. return;
  543. }
  544. if (!scene.pointerMovePredicate) {
  545. scene.pointerMovePredicate = (mesh: AbstractMesh): boolean =>
  546. mesh.isPickable &&
  547. mesh.isVisible &&
  548. mesh.isReady() &&
  549. mesh.isEnabled() &&
  550. (mesh.enablePointerMoveEvents || scene.constantlyUpdateMeshUnderPointer || mesh._getActionManagerForTrigger() != null) &&
  551. (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
  552. }
  553. // Meshes
  554. var pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerMovePredicate, false, scene.cameraToUseForPointers);
  555. this._processPointerMove(pickResult, evt);
  556. };
  557. this._onPointerDown = (evt: PointerEvent) => {
  558. this._totalPointersPressed++;
  559. this._pickedDownMesh = null;
  560. this._meshPickProceed = false;
  561. // preserve compatibility with Safari when pointerId is not present
  562. if (evt.pointerId === undefined) {
  563. (evt as any).pointerId = 0;
  564. }
  565. this._updatePointerPosition(evt);
  566. if (scene.preventDefaultOnPointerDown && elementToAttachTo) {
  567. evt.preventDefault();
  568. elementToAttachTo.focus();
  569. }
  570. this._startingPointerPosition.x = this._pointerX;
  571. this._startingPointerPosition.y = this._pointerY;
  572. this._startingPointerTime = Date.now();
  573. // PreObservable support
  574. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOWN)) {
  575. return;
  576. }
  577. if (!scene.cameraToUseForPointers && !scene.activeCamera) {
  578. return;
  579. }
  580. this._pointerCaptures[evt.pointerId] = true;
  581. if (!scene.pointerDownPredicate) {
  582. scene.pointerDownPredicate = (mesh: AbstractMesh): boolean => {
  583. return mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
  584. };
  585. }
  586. // Meshes
  587. this._pickedDownMesh = null;
  588. var pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerDownPredicate, false, scene.cameraToUseForPointers);
  589. this._processPointerDown(pickResult, evt);
  590. };
  591. this._onPointerUp = (evt: PointerEvent) => {
  592. if (this._totalPointersPressed === 0) {
  593. // We are attaching the pointer up to windows because of a bug in FF
  594. return; // So we need to test it the pointer down was pressed before.
  595. }
  596. this._totalPointersPressed--;
  597. this._pickedUpMesh = null;
  598. this._meshPickProceed = false;
  599. // preserve compatibility with Safari when pointerId is not present
  600. if (evt.pointerId === undefined) {
  601. (evt as any).pointerId = 0;
  602. }
  603. this._updatePointerPosition(evt);
  604. if (scene.preventDefaultOnPointerUp && elementToAttachTo) {
  605. evt.preventDefault();
  606. elementToAttachTo.focus();
  607. }
  608. this._initClickEvent(scene.onPrePointerObservable, scene.onPointerObservable, evt, (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => {
  609. // PreObservable support
  610. if (scene.onPrePointerObservable.hasObservers()) {
  611. if (!clickInfo.ignore) {
  612. if (!clickInfo.hasSwiped) {
  613. if (clickInfo.singleClick && scene.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERTAP)) {
  614. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERTAP)) {
  615. return;
  616. }
  617. }
  618. if (clickInfo.doubleClick && scene.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP)) {
  619. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOUBLETAP)) {
  620. return;
  621. }
  622. }
  623. }
  624. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERUP)) {
  625. return;
  626. }
  627. }
  628. }
  629. if (!this._pointerCaptures[evt.pointerId]) {
  630. return;
  631. }
  632. this._pointerCaptures[evt.pointerId] = false;
  633. if (!scene.cameraToUseForPointers && !scene.activeCamera) {
  634. return;
  635. }
  636. if (!scene.pointerUpPredicate) {
  637. scene.pointerUpPredicate = (mesh: AbstractMesh): boolean => {
  638. return mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
  639. };
  640. }
  641. // Meshes
  642. if (!this._meshPickProceed && ((AbstractActionManager && AbstractActionManager.HasTriggers) || scene.onPointerObservable.hasObservers())) {
  643. this._initActionManager(null, clickInfo);
  644. }
  645. if (!pickResult) {
  646. pickResult = this._currentPickResult;
  647. }
  648. this._processPointerUp(pickResult, evt, clickInfo);
  649. this._previousPickResult = this._currentPickResult;
  650. });
  651. };
  652. this._onKeyDown = (evt: KeyboardEvent) => {
  653. let type = KeyboardEventTypes.KEYDOWN;
  654. if (scene.onPreKeyboardObservable.hasObservers()) {
  655. let pi = new KeyboardInfoPre(type, evt);
  656. scene.onPreKeyboardObservable.notifyObservers(pi, type);
  657. if (pi.skipOnPointerObservable) {
  658. return;
  659. }
  660. }
  661. if (scene.onKeyboardObservable.hasObservers()) {
  662. let pi = new KeyboardInfo(type, evt);
  663. scene.onKeyboardObservable.notifyObservers(pi, type);
  664. }
  665. if (scene.actionManager) {
  666. scene.actionManager.processTrigger(Constants.ACTION_OnKeyDownTrigger, ActionEvent.CreateNewFromScene(scene, evt));
  667. }
  668. };
  669. this._onKeyUp = (evt: KeyboardEvent) => {
  670. let type = KeyboardEventTypes.KEYUP;
  671. if (scene.onPreKeyboardObservable.hasObservers()) {
  672. let pi = new KeyboardInfoPre(type, evt);
  673. scene.onPreKeyboardObservable.notifyObservers(pi, type);
  674. if (pi.skipOnPointerObservable) {
  675. return;
  676. }
  677. }
  678. if (scene.onKeyboardObservable.hasObservers()) {
  679. let pi = new KeyboardInfo(type, evt);
  680. scene.onKeyboardObservable.notifyObservers(pi, type);
  681. }
  682. if (scene.actionManager) {
  683. scene.actionManager.processTrigger(Constants.ACTION_OnKeyUpTrigger, ActionEvent.CreateNewFromScene(scene, evt));
  684. }
  685. };
  686. // Keyboard events
  687. this._onCanvasFocusObserver = engine.onCanvasFocusObservable.add(
  688. (() => {
  689. let fn = () => {
  690. if (!elementToAttachTo) {
  691. return;
  692. }
  693. elementToAttachTo.addEventListener("keydown", this._onKeyDown, false);
  694. elementToAttachTo.addEventListener("keyup", this._onKeyUp, false);
  695. };
  696. if (document.activeElement === elementToAttachTo) {
  697. fn();
  698. }
  699. return fn;
  700. })()
  701. );
  702. this._onCanvasBlurObserver = engine.onCanvasBlurObservable.add(() => {
  703. if (!elementToAttachTo) {
  704. return;
  705. }
  706. elementToAttachTo.removeEventListener("keydown", this._onKeyDown);
  707. elementToAttachTo.removeEventListener("keyup", this._onKeyUp);
  708. });
  709. // Pointer events
  710. var eventPrefix = Tools.GetPointerPrefix(engine);
  711. if (attachMove) {
  712. elementToAttachTo.addEventListener(eventPrefix + "move", <any>this._onPointerMove, false);
  713. // Wheel
  714. this._wheelEventName =
  715. "onwheel" in document.createElement("div")
  716. ? "wheel" // Modern browsers support "wheel"
  717. : (<any>document).onmousewheel !== undefined
  718. ? "mousewheel" // Webkit and IE support at least "mousewheel"
  719. : "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox
  720. elementToAttachTo.addEventListener(this._wheelEventName, <any>this._onPointerMove, false);
  721. }
  722. if (attachDown) {
  723. elementToAttachTo.addEventListener(eventPrefix + "down", <any>this._onPointerDown, false);
  724. }
  725. if (attachUp) {
  726. let hostWindow = scene.getEngine().getHostWindow();
  727. if (hostWindow) {
  728. hostWindow.addEventListener(eventPrefix + "up", <any>this._onPointerUp, false);
  729. }
  730. }
  731. this._alreadyAttached = true;
  732. }
  733. /**
  734. * Detaches all event handlers
  735. */
  736. public detachControl() {
  737. const canvas = this._scene.getEngine().getInputElement();
  738. const engine = this._scene.getEngine();
  739. const eventPrefix = Tools.GetPointerPrefix(engine);
  740. if (!canvas) {
  741. return;
  742. }
  743. if (!this._alreadyAttached) {
  744. return;
  745. }
  746. // Pointer
  747. canvas.removeEventListener(eventPrefix + "move", <any>this._onPointerMove);
  748. canvas.removeEventListener(this._wheelEventName, <any>this._onPointerMove);
  749. canvas.removeEventListener(eventPrefix + "down", <any>this._onPointerDown);
  750. window.removeEventListener(eventPrefix + "up", <any>this._onPointerUp);
  751. // Blur / Focus
  752. if (this._onCanvasBlurObserver) {
  753. engine.onCanvasBlurObservable.remove(this._onCanvasBlurObserver);
  754. }
  755. if (this._onCanvasFocusObserver) {
  756. engine.onCanvasFocusObservable.remove(this._onCanvasFocusObserver);
  757. }
  758. // Keyboard
  759. canvas.removeEventListener("keydown", this._onKeyDown);
  760. canvas.removeEventListener("keyup", this._onKeyUp);
  761. // Cursor
  762. if (!this._scene.doNotHandleCursors) {
  763. canvas.style.cursor = this._scene.defaultCursor;
  764. }
  765. this._alreadyAttached = false;
  766. }
  767. /**
  768. * Force the value of meshUnderPointer
  769. * @param mesh defines the mesh to use
  770. * @param pointerId optional pointer id when using more than one pointer. Defaults to 0
  771. */
  772. public setPointerOverMesh(mesh: Nullable<AbstractMesh>, pointerId: number = 0): void {
  773. // Sanity check
  774. if (pointerId < 0) {
  775. pointerId = 0;
  776. }
  777. if (this._meshUnderPointerId[pointerId] === mesh) {
  778. return;
  779. }
  780. let underPointerMesh = this._meshUnderPointerId[pointerId];
  781. let actionManager: Nullable<AbstractActionManager>;
  782. if (underPointerMesh) {
  783. actionManager = underPointerMesh._getActionManagerForTrigger(Constants.ACTION_OnPointerOutTrigger);
  784. if (actionManager) {
  785. actionManager.processTrigger(Constants.ACTION_OnPointerOutTrigger, ActionEvent.CreateNew(underPointerMesh, undefined, { pointerId }));
  786. }
  787. }
  788. this._meshUnderPointerId[pointerId] = mesh;
  789. this._pointerOverMesh = mesh;
  790. underPointerMesh = this._meshUnderPointerId[pointerId];
  791. if (underPointerMesh) {
  792. actionManager = underPointerMesh._getActionManagerForTrigger(Constants.ACTION_OnPointerOverTrigger);
  793. if (actionManager) {
  794. actionManager.processTrigger(Constants.ACTION_OnPointerOverTrigger, ActionEvent.CreateNew(underPointerMesh, undefined, { pointerId }));
  795. }
  796. }
  797. }
  798. /**
  799. * Gets the mesh under the pointer
  800. * @returns a Mesh or null if no mesh is under the pointer
  801. */
  802. public getPointerOverMesh(): Nullable<AbstractMesh> {
  803. return this._pointerOverMesh;
  804. }
  805. }