scene.inputManager.js 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969
  1. import { PointerInfoPre, PointerInfo, PointerEventTypes } from "../Events/pointerEvents.js";
  2. import { AbstractActionManager } from "../Actions/abstractActionManager.js";
  3. import { PickingInfo } from "../Collisions/pickingInfo.js";
  4. import { Vector2, Matrix } from "../Maths/math.vector.js";
  5. import { ActionEvent } from "../Actions/actionEvent.js";
  6. import { KeyboardEventTypes, KeyboardInfoPre, KeyboardInfo } from "../Events/keyboardEvents.js";
  7. import { DeviceType, PointerInput } from "../DeviceInput/InputDevices/deviceEnums.js";
  8. import { DeviceSourceManager } from "../DeviceInput/InputDevices/deviceSourceManager.js";
  9. import { EngineStore } from "../Engines/engineStore.js";
  10. /** @internal */
  11. // eslint-disable-next-line @typescript-eslint/naming-convention
  12. class _ClickInfo {
  13. constructor() {
  14. this._singleClick = false;
  15. this._doubleClick = false;
  16. this._hasSwiped = false;
  17. this._ignore = false;
  18. }
  19. get singleClick() {
  20. return this._singleClick;
  21. }
  22. get doubleClick() {
  23. return this._doubleClick;
  24. }
  25. get hasSwiped() {
  26. return this._hasSwiped;
  27. }
  28. get ignore() {
  29. return this._ignore;
  30. }
  31. set singleClick(b) {
  32. this._singleClick = b;
  33. }
  34. set doubleClick(b) {
  35. this._doubleClick = b;
  36. }
  37. set hasSwiped(b) {
  38. this._hasSwiped = b;
  39. }
  40. set ignore(b) {
  41. this._ignore = b;
  42. }
  43. }
  44. /**
  45. * Class used to manage all inputs for the scene.
  46. */
  47. export class InputManager {
  48. /**
  49. * Creates a new InputManager
  50. * @param scene - defines the hosting scene
  51. */
  52. constructor(scene) {
  53. /** 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. */
  54. this._alreadyAttached = false;
  55. this._meshPickProceed = false;
  56. this._currentPickResult = null;
  57. this._previousPickResult = null;
  58. this._totalPointersPressed = 0;
  59. this._doubleClickOccured = false;
  60. this._isSwiping = false;
  61. this._swipeButtonPressed = -1;
  62. this._skipPointerTap = false;
  63. this._isMultiTouchGesture = false;
  64. this._pointerX = 0;
  65. this._pointerY = 0;
  66. this._startingPointerPosition = new Vector2(0, 0);
  67. this._previousStartingPointerPosition = new Vector2(0, 0);
  68. this._startingPointerTime = 0;
  69. this._previousStartingPointerTime = 0;
  70. this._pointerCaptures = {};
  71. this._meshUnderPointerId = {};
  72. this._movePointerInfo = null;
  73. this._cameraObserverCount = 0;
  74. this._delayedClicks = [null, null, null, null, null];
  75. this._deviceSourceManager = null;
  76. this._scene = scene || EngineStore.LastCreatedScene;
  77. if (!this._scene) {
  78. return;
  79. }
  80. }
  81. /**
  82. * Gets the mesh that is currently under the pointer
  83. * @returns Mesh that the pointer is pointer is hovering over
  84. */
  85. get meshUnderPointer() {
  86. if (this._movePointerInfo) {
  87. // Because _pointerOverMesh is populated as part of _pickMove, we need to force a pick to update it.
  88. // Calling _pickMove calls _setCursorAndPointerOverMesh which calls setPointerOverMesh
  89. this._movePointerInfo._generatePickInfo();
  90. // Once we have what we need, we can clear _movePointerInfo because we don't need it anymore
  91. this._movePointerInfo = null;
  92. }
  93. return this._pointerOverMesh;
  94. }
  95. /**
  96. * When using more than one pointer (for example in XR) you can get the mesh under the specific pointer
  97. * @param pointerId - the pointer id to use
  98. * @returns The mesh under this pointer id or null if not found
  99. */
  100. getMeshUnderPointerByPointerId(pointerId) {
  101. return this._meshUnderPointerId[pointerId] || null;
  102. }
  103. /**
  104. * Gets the pointer coordinates in 2D without any translation (ie. straight out of the pointer event)
  105. * @returns Vector with X/Y values directly from pointer event
  106. */
  107. get unTranslatedPointer() {
  108. return new Vector2(this._unTranslatedPointerX, this._unTranslatedPointerY);
  109. }
  110. /**
  111. * Gets or sets the current on-screen X position of the pointer
  112. * @returns Translated X with respect to screen
  113. */
  114. get pointerX() {
  115. return this._pointerX;
  116. }
  117. set pointerX(value) {
  118. this._pointerX = value;
  119. }
  120. /**
  121. * Gets or sets the current on-screen Y position of the pointer
  122. * @returns Translated Y with respect to screen
  123. */
  124. get pointerY() {
  125. return this._pointerY;
  126. }
  127. set pointerY(value) {
  128. this._pointerY = value;
  129. }
  130. _updatePointerPosition(evt) {
  131. const canvasRect = this._scene.getEngine().getInputElementClientRect();
  132. if (!canvasRect) {
  133. return;
  134. }
  135. this._pointerX = evt.clientX - canvasRect.left;
  136. this._pointerY = evt.clientY - canvasRect.top;
  137. this._unTranslatedPointerX = this._pointerX;
  138. this._unTranslatedPointerY = this._pointerY;
  139. }
  140. _processPointerMove(pickResult, evt) {
  141. const scene = this._scene;
  142. const engine = scene.getEngine();
  143. const canvas = engine.getInputElement();
  144. if (canvas) {
  145. canvas.tabIndex = engine.canvasTabIndex;
  146. // Restore pointer
  147. if (!scene.doNotHandleCursors) {
  148. canvas.style.cursor = scene.defaultCursor;
  149. }
  150. }
  151. this._setCursorAndPointerOverMesh(pickResult, evt, scene);
  152. for (const step of scene._pointerMoveStage) {
  153. // If _pointerMoveState is defined, we have an active spriteManager and can't use Lazy Picking
  154. // Therefore, we need to force a pick to update the pickResult
  155. pickResult = pickResult || this._pickMove(evt);
  156. const isMeshPicked = pickResult?.pickedMesh ? true : false;
  157. pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, isMeshPicked, canvas);
  158. }
  159. const type = evt.inputIndex >= PointerInput.MouseWheelX && evt.inputIndex <= PointerInput.MouseWheelZ ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE;
  160. if (scene.onPointerMove) {
  161. // Because of lazy picking, we need to force a pick to update the pickResult
  162. pickResult = pickResult || this._pickMove(evt);
  163. scene.onPointerMove(evt, pickResult, type);
  164. }
  165. let pointerInfo;
  166. if (pickResult) {
  167. pointerInfo = new PointerInfo(type, evt, pickResult);
  168. this._setRayOnPointerInfo(pickResult, evt);
  169. }
  170. else {
  171. pointerInfo = new PointerInfo(type, evt, null, this);
  172. this._movePointerInfo = pointerInfo;
  173. }
  174. if (scene.onPointerObservable.hasObservers()) {
  175. scene.onPointerObservable.notifyObservers(pointerInfo, type);
  176. }
  177. }
  178. // Pointers handling
  179. /** @internal */
  180. _setRayOnPointerInfo(pickInfo, event) {
  181. const scene = this._scene;
  182. if (pickInfo && scene._pickingAvailable) {
  183. if (!pickInfo.ray) {
  184. pickInfo.ray = scene.createPickingRay(event.offsetX, event.offsetY, Matrix.Identity(), scene.activeCamera);
  185. }
  186. }
  187. }
  188. /** @internal */
  189. _addCameraPointerObserver(observer, mask) {
  190. this._cameraObserverCount++;
  191. return this._scene.onPointerObservable.add(observer, mask);
  192. }
  193. /** @internal */
  194. _removeCameraPointerObserver(observer) {
  195. this._cameraObserverCount--;
  196. return this._scene.onPointerObservable.remove(observer);
  197. }
  198. _checkForPicking() {
  199. return !!(this._scene.onPointerObservable.observers.length > this._cameraObserverCount || this._scene.onPointerPick);
  200. }
  201. _checkPrePointerObservable(pickResult, evt, type) {
  202. const scene = this._scene;
  203. const pi = new PointerInfoPre(type, evt, this._unTranslatedPointerX, this._unTranslatedPointerY);
  204. if (pickResult) {
  205. pi.originalPickingInfo = pickResult;
  206. pi.ray = pickResult.ray;
  207. if (evt.pointerType === "xr-near" && pickResult.originMesh) {
  208. pi.nearInteractionPickingInfo = pickResult;
  209. }
  210. }
  211. scene.onPrePointerObservable.notifyObservers(pi, type);
  212. if (pi.skipOnPointerObservable) {
  213. return true;
  214. }
  215. else {
  216. return false;
  217. }
  218. }
  219. /** @internal */
  220. _pickMove(evt) {
  221. const scene = this._scene;
  222. const pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerMovePredicate, scene.pointerMoveFastCheck, scene.cameraToUseForPointers, scene.pointerMoveTrianglePredicate);
  223. this._setCursorAndPointerOverMesh(pickResult, evt, scene);
  224. return pickResult;
  225. }
  226. _setCursorAndPointerOverMesh(pickResult, evt, scene) {
  227. const engine = scene.getEngine();
  228. const canvas = engine.getInputElement();
  229. if (pickResult?.pickedMesh) {
  230. this.setPointerOverMesh(pickResult.pickedMesh, evt.pointerId, pickResult, evt);
  231. if (!scene.doNotHandleCursors && canvas && this._pointerOverMesh) {
  232. const actionManager = this._pointerOverMesh._getActionManagerForTrigger();
  233. if (actionManager && actionManager.hasPointerTriggers) {
  234. canvas.style.cursor = actionManager.hoverCursor || scene.hoverCursor;
  235. }
  236. }
  237. }
  238. else {
  239. this.setPointerOverMesh(null, evt.pointerId, pickResult, evt);
  240. }
  241. }
  242. /**
  243. * Use this method to simulate a pointer move on a mesh
  244. * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
  245. * @param pickResult - pickingInfo of the object wished to simulate pointer event on
  246. * @param pointerEventInit - pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
  247. */
  248. simulatePointerMove(pickResult, pointerEventInit) {
  249. const evt = new PointerEvent("pointermove", pointerEventInit);
  250. evt.inputIndex = PointerInput.Move;
  251. if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERMOVE)) {
  252. return;
  253. }
  254. this._processPointerMove(pickResult, evt);
  255. }
  256. /**
  257. * Use this method to simulate a pointer down on a mesh
  258. * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
  259. * @param pickResult - pickingInfo of the object wished to simulate pointer event on
  260. * @param pointerEventInit - pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
  261. */
  262. simulatePointerDown(pickResult, pointerEventInit) {
  263. const evt = new PointerEvent("pointerdown", pointerEventInit);
  264. evt.inputIndex = evt.button + 2;
  265. if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERDOWN)) {
  266. return;
  267. }
  268. this._processPointerDown(pickResult, evt);
  269. }
  270. _processPointerDown(pickResult, evt) {
  271. const scene = this._scene;
  272. if (pickResult?.pickedMesh) {
  273. this._pickedDownMesh = pickResult.pickedMesh;
  274. const actionManager = pickResult.pickedMesh._getActionManagerForTrigger();
  275. if (actionManager) {
  276. if (actionManager.hasPickTriggers) {
  277. actionManager.processTrigger(5, ActionEvent.CreateNew(pickResult.pickedMesh, evt, pickResult));
  278. switch (evt.button) {
  279. case 0:
  280. actionManager.processTrigger(2, ActionEvent.CreateNew(pickResult.pickedMesh, evt, pickResult));
  281. break;
  282. case 1:
  283. actionManager.processTrigger(4, ActionEvent.CreateNew(pickResult.pickedMesh, evt, pickResult));
  284. break;
  285. case 2:
  286. actionManager.processTrigger(3, ActionEvent.CreateNew(pickResult.pickedMesh, evt, pickResult));
  287. break;
  288. }
  289. }
  290. if (actionManager.hasSpecificTrigger(8)) {
  291. window.setTimeout(() => {
  292. const pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, (mesh) => ((mesh.isPickable &&
  293. mesh.isVisible &&
  294. mesh.isReady() &&
  295. mesh.actionManager &&
  296. mesh.actionManager.hasSpecificTrigger(8) &&
  297. mesh === this._pickedDownMesh)), false, scene.cameraToUseForPointers);
  298. if (pickResult?.pickedMesh && actionManager) {
  299. if (this._totalPointersPressed !== 0 && Date.now() - this._startingPointerTime > InputManager.LongPressDelay && !this._isPointerSwiping()) {
  300. this._startingPointerTime = 0;
  301. actionManager.processTrigger(8, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
  302. }
  303. }
  304. }, InputManager.LongPressDelay);
  305. }
  306. }
  307. }
  308. else {
  309. for (const step of scene._pointerDownStage) {
  310. pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt, false);
  311. }
  312. }
  313. let pointerInfo;
  314. const type = PointerEventTypes.POINTERDOWN;
  315. if (pickResult) {
  316. if (scene.onPointerDown) {
  317. scene.onPointerDown(evt, pickResult, type);
  318. }
  319. pointerInfo = new PointerInfo(type, evt, pickResult);
  320. this._setRayOnPointerInfo(pickResult, evt);
  321. }
  322. else {
  323. pointerInfo = new PointerInfo(type, evt, null, this);
  324. }
  325. if (scene.onPointerObservable.hasObservers()) {
  326. scene.onPointerObservable.notifyObservers(pointerInfo, type);
  327. }
  328. }
  329. /**
  330. * @internal
  331. * @internals Boolean if delta for pointer exceeds drag movement threshold
  332. */
  333. _isPointerSwiping() {
  334. return this._isSwiping;
  335. }
  336. /**
  337. * Use this method to simulate a pointer up on a mesh
  338. * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
  339. * @param pickResult - pickingInfo of the object wished to simulate pointer event on
  340. * @param pointerEventInit - pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
  341. * @param doubleTap - indicates that the pointer up event should be considered as part of a double click (false by default)
  342. */
  343. simulatePointerUp(pickResult, pointerEventInit, doubleTap) {
  344. const evt = new PointerEvent("pointerup", pointerEventInit);
  345. evt.inputIndex = PointerInput.Move;
  346. const clickInfo = new _ClickInfo();
  347. if (doubleTap) {
  348. clickInfo.doubleClick = true;
  349. }
  350. else {
  351. clickInfo.singleClick = true;
  352. }
  353. if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERUP)) {
  354. return;
  355. }
  356. this._processPointerUp(pickResult, evt, clickInfo);
  357. }
  358. _processPointerUp(pickResult, evt, clickInfo) {
  359. const scene = this._scene;
  360. if (pickResult?.pickedMesh) {
  361. this._pickedUpMesh = pickResult.pickedMesh;
  362. if (this._pickedDownMesh === this._pickedUpMesh) {
  363. if (scene.onPointerPick) {
  364. scene.onPointerPick(evt, pickResult);
  365. }
  366. if (clickInfo.singleClick && !clickInfo.ignore && scene.onPointerObservable.observers.length > this._cameraObserverCount) {
  367. const type = PointerEventTypes.POINTERPICK;
  368. const pi = new PointerInfo(type, evt, pickResult);
  369. this._setRayOnPointerInfo(pickResult, evt);
  370. scene.onPointerObservable.notifyObservers(pi, type);
  371. }
  372. }
  373. const actionManager = pickResult.pickedMesh._getActionManagerForTrigger();
  374. if (actionManager && !clickInfo.ignore) {
  375. actionManager.processTrigger(7, ActionEvent.CreateNew(pickResult.pickedMesh, evt, pickResult));
  376. if (!clickInfo.hasSwiped && clickInfo.singleClick) {
  377. actionManager.processTrigger(1, ActionEvent.CreateNew(pickResult.pickedMesh, evt, pickResult));
  378. }
  379. const doubleClickActionManager = pickResult.pickedMesh._getActionManagerForTrigger(6);
  380. if (clickInfo.doubleClick && doubleClickActionManager) {
  381. doubleClickActionManager.processTrigger(6, ActionEvent.CreateNew(pickResult.pickedMesh, evt, pickResult));
  382. }
  383. }
  384. }
  385. else {
  386. if (!clickInfo.ignore) {
  387. for (const step of scene._pointerUpStage) {
  388. pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt, clickInfo.doubleClick);
  389. }
  390. }
  391. }
  392. if (this._pickedDownMesh && this._pickedDownMesh !== this._pickedUpMesh) {
  393. const pickedDownActionManager = this._pickedDownMesh._getActionManagerForTrigger(16);
  394. if (pickedDownActionManager) {
  395. pickedDownActionManager.processTrigger(16, ActionEvent.CreateNew(this._pickedDownMesh, evt));
  396. }
  397. }
  398. if (!clickInfo.ignore) {
  399. const pi = new PointerInfo(PointerEventTypes.POINTERUP, evt, pickResult);
  400. // Set ray on picking info. Note that this info will also be reused for the tap notification.
  401. this._setRayOnPointerInfo(pickResult, evt);
  402. scene.onPointerObservable.notifyObservers(pi, PointerEventTypes.POINTERUP);
  403. if (scene.onPointerUp) {
  404. scene.onPointerUp(evt, pickResult, PointerEventTypes.POINTERUP);
  405. }
  406. if (!clickInfo.hasSwiped && !this._skipPointerTap && !this._isMultiTouchGesture) {
  407. let type = 0;
  408. if (clickInfo.singleClick) {
  409. type = PointerEventTypes.POINTERTAP;
  410. }
  411. else if (clickInfo.doubleClick) {
  412. type = PointerEventTypes.POINTERDOUBLETAP;
  413. }
  414. if (type) {
  415. const pi = new PointerInfo(type, evt, pickResult);
  416. if (scene.onPointerObservable.hasObservers() && scene.onPointerObservable.hasSpecificMask(type)) {
  417. scene.onPointerObservable.notifyObservers(pi, type);
  418. }
  419. }
  420. }
  421. }
  422. }
  423. /**
  424. * Gets a boolean indicating if the current pointer event is captured (meaning that the scene has already handled the pointer down)
  425. * @param pointerId - defines the pointer id to use in a multi-touch scenario (0 by default)
  426. * @returns true if the pointer was captured
  427. */
  428. isPointerCaptured(pointerId = 0) {
  429. return this._pointerCaptures[pointerId];
  430. }
  431. /**
  432. * Attach events to the canvas (To handle actionManagers triggers and raise onPointerMove, onPointerDown and onPointerUp
  433. * @param attachUp - defines if you want to attach events to pointerup
  434. * @param attachDown - defines if you want to attach events to pointerdown
  435. * @param attachMove - defines if you want to attach events to pointermove
  436. * @param elementToAttachTo - defines the target DOM element to attach to (will use the canvas by default)
  437. */
  438. attachControl(attachUp = true, attachDown = true, attachMove = true, elementToAttachTo = null) {
  439. const scene = this._scene;
  440. const engine = scene.getEngine();
  441. if (!elementToAttachTo) {
  442. elementToAttachTo = engine.getInputElement();
  443. }
  444. if (this._alreadyAttached) {
  445. this.detachControl();
  446. }
  447. if (elementToAttachTo) {
  448. this._alreadyAttachedTo = elementToAttachTo;
  449. }
  450. this._deviceSourceManager = new DeviceSourceManager(engine);
  451. // Because this is only called from _initClickEvent, which is called in _onPointerUp, we'll use the pointerUpPredicate for the pick call
  452. this._initActionManager = (act) => {
  453. if (!this._meshPickProceed) {
  454. const pickResult = scene.skipPointerUpPicking || (scene._registeredActions === 0 && !this._checkForPicking() && !scene.onPointerUp)
  455. ? null
  456. : scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerUpPredicate, scene.pointerUpFastCheck, scene.cameraToUseForPointers, scene.pointerUpTrianglePredicate);
  457. this._currentPickResult = pickResult;
  458. if (pickResult) {
  459. act = pickResult.hit && pickResult.pickedMesh ? pickResult.pickedMesh._getActionManagerForTrigger() : null;
  460. }
  461. this._meshPickProceed = true;
  462. }
  463. return act;
  464. };
  465. this._delayedSimpleClick = (btn, clickInfo, cb) => {
  466. // double click delay is over and that no double click has been raised since, or the 2 consecutive keys pressed are different
  467. if ((Date.now() - this._previousStartingPointerTime > InputManager.DoubleClickDelay && !this._doubleClickOccured) || btn !== this._previousButtonPressed) {
  468. this._doubleClickOccured = false;
  469. clickInfo.singleClick = true;
  470. clickInfo.ignore = false;
  471. // If we have a delayed click, we need to resolve the TAP event
  472. if (this._delayedClicks[btn]) {
  473. const evt = this._delayedClicks[btn].evt;
  474. const type = PointerEventTypes.POINTERTAP;
  475. const pi = new PointerInfo(type, evt, this._currentPickResult);
  476. if (scene.onPointerObservable.hasObservers() && scene.onPointerObservable.hasSpecificMask(type)) {
  477. scene.onPointerObservable.notifyObservers(pi, type);
  478. }
  479. // Clear the delayed click
  480. this._delayedClicks[btn] = null;
  481. }
  482. }
  483. };
  484. this._initClickEvent = (obs1, obs2, evt, cb) => {
  485. const clickInfo = new _ClickInfo();
  486. this._currentPickResult = null;
  487. let act = null;
  488. let checkPicking = obs1.hasSpecificMask(PointerEventTypes.POINTERPICK) ||
  489. obs2.hasSpecificMask(PointerEventTypes.POINTERPICK) ||
  490. obs1.hasSpecificMask(PointerEventTypes.POINTERTAP) ||
  491. obs2.hasSpecificMask(PointerEventTypes.POINTERTAP) ||
  492. obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) ||
  493. obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
  494. if (!checkPicking && AbstractActionManager) {
  495. act = this._initActionManager(act, clickInfo);
  496. if (act) {
  497. checkPicking = act.hasPickTriggers;
  498. }
  499. }
  500. let needToIgnoreNext = false;
  501. if (checkPicking) {
  502. const btn = evt.button;
  503. clickInfo.hasSwiped = this._isPointerSwiping();
  504. if (!clickInfo.hasSwiped) {
  505. let checkSingleClickImmediately = !InputManager.ExclusiveDoubleClickMode;
  506. if (!checkSingleClickImmediately) {
  507. checkSingleClickImmediately = !obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) && !obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
  508. if (checkSingleClickImmediately && !AbstractActionManager.HasSpecificTrigger(6)) {
  509. act = this._initActionManager(act, clickInfo);
  510. if (act) {
  511. checkSingleClickImmediately = !act.hasSpecificTrigger(6);
  512. }
  513. }
  514. }
  515. if (checkSingleClickImmediately) {
  516. // single click detected if double click delay is over or two different successive keys pressed without exclusive double click or no double click required
  517. if (Date.now() - this._previousStartingPointerTime > InputManager.DoubleClickDelay || btn !== this._previousButtonPressed) {
  518. clickInfo.singleClick = true;
  519. cb(clickInfo, this._currentPickResult);
  520. needToIgnoreNext = true;
  521. }
  522. }
  523. // at least one double click is required to be check and exclusive double click is enabled
  524. else {
  525. // Queue up a delayed click, just in case this isn't a double click
  526. // It should be noted that while this delayed event happens
  527. // because of user input, it shouldn't be considered as a direct,
  528. // timing-dependent result of that input. It's meant to just fire the TAP event
  529. const delayedClick = {
  530. evt: evt,
  531. clickInfo: clickInfo,
  532. timeoutId: window.setTimeout(this._delayedSimpleClick.bind(this, btn, clickInfo, cb), InputManager.DoubleClickDelay),
  533. };
  534. this._delayedClicks[btn] = delayedClick;
  535. }
  536. let checkDoubleClick = obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) || obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
  537. if (!checkDoubleClick && AbstractActionManager.HasSpecificTrigger(6)) {
  538. act = this._initActionManager(act, clickInfo);
  539. if (act) {
  540. checkDoubleClick = act.hasSpecificTrigger(6);
  541. }
  542. }
  543. if (checkDoubleClick) {
  544. // two successive keys pressed are equal, double click delay is not over and double click has not just occurred
  545. if (btn === this._previousButtonPressed && Date.now() - this._previousStartingPointerTime < InputManager.DoubleClickDelay && !this._doubleClickOccured) {
  546. // pointer has not moved for 2 clicks, it's a double click
  547. if (!clickInfo.hasSwiped && !this._isPointerSwiping()) {
  548. this._previousStartingPointerTime = 0;
  549. this._doubleClickOccured = true;
  550. clickInfo.doubleClick = true;
  551. clickInfo.ignore = false;
  552. // If we have a pending click, we need to cancel it
  553. if (InputManager.ExclusiveDoubleClickMode && this._delayedClicks[btn]) {
  554. clearTimeout(this._delayedClicks[btn]?.timeoutId);
  555. this._delayedClicks[btn] = null;
  556. }
  557. cb(clickInfo, this._currentPickResult);
  558. }
  559. // if the two successive clicks are too far, it's just two simple clicks
  560. else {
  561. this._doubleClickOccured = false;
  562. this._previousStartingPointerTime = this._startingPointerTime;
  563. this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
  564. this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
  565. this._previousButtonPressed = btn;
  566. if (InputManager.ExclusiveDoubleClickMode) {
  567. // If we have a delayed click, we need to cancel it
  568. if (this._delayedClicks[btn]) {
  569. clearTimeout(this._delayedClicks[btn]?.timeoutId);
  570. this._delayedClicks[btn] = null;
  571. }
  572. cb(clickInfo, this._previousPickResult);
  573. }
  574. else {
  575. cb(clickInfo, this._currentPickResult);
  576. }
  577. }
  578. needToIgnoreNext = true;
  579. }
  580. // just the first click of the double has been raised
  581. else {
  582. this._doubleClickOccured = false;
  583. this._previousStartingPointerTime = this._startingPointerTime;
  584. this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
  585. this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
  586. this._previousButtonPressed = btn;
  587. }
  588. }
  589. }
  590. }
  591. // Even if ExclusiveDoubleClickMode is true, we need to always handle
  592. // up events at time of execution, unless we're explicitly ignoring them.
  593. if (!needToIgnoreNext) {
  594. cb(clickInfo, this._currentPickResult);
  595. }
  596. };
  597. this._onPointerMove = (evt) => {
  598. this._updatePointerPosition(evt);
  599. // Check if pointer leaves DragMovementThreshold range to determine if swipe is occurring
  600. if (!this._isSwiping && this._swipeButtonPressed !== -1) {
  601. this._isSwiping =
  602. Math.abs(this._startingPointerPosition.x - this._pointerX) > InputManager.DragMovementThreshold ||
  603. Math.abs(this._startingPointerPosition.y - this._pointerY) > InputManager.DragMovementThreshold;
  604. }
  605. // Because there's a race condition between pointermove and pointerlockchange events, we need to
  606. // verify that the pointer is still locked after each pointermove event.
  607. if (engine.isPointerLock) {
  608. engine._verifyPointerLock();
  609. }
  610. // PreObservable support
  611. if (this._checkPrePointerObservable(null, evt, evt.inputIndex >= PointerInput.MouseWheelX && evt.inputIndex <= PointerInput.MouseWheelZ ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE)) {
  612. return;
  613. }
  614. if (!scene.cameraToUseForPointers && !scene.activeCamera) {
  615. return;
  616. }
  617. if (scene.skipPointerMovePicking) {
  618. this._processPointerMove(new PickingInfo(), evt);
  619. return;
  620. }
  621. if (!scene.pointerMovePredicate) {
  622. scene.pointerMovePredicate = (mesh) => mesh.isPickable &&
  623. mesh.isVisible &&
  624. mesh.isReady() &&
  625. mesh.isEnabled() &&
  626. (mesh.enablePointerMoveEvents || scene.constantlyUpdateMeshUnderPointer || mesh._getActionManagerForTrigger() !== null) &&
  627. (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
  628. }
  629. const pickResult = scene._registeredActions > 0 || scene.constantlyUpdateMeshUnderPointer ? this._pickMove(evt) : null;
  630. this._processPointerMove(pickResult, evt);
  631. };
  632. this._onPointerDown = (evt) => {
  633. this._totalPointersPressed++;
  634. this._pickedDownMesh = null;
  635. this._meshPickProceed = false;
  636. // If ExclusiveDoubleClickMode is true, we need to resolve any pending delayed clicks
  637. if (InputManager.ExclusiveDoubleClickMode) {
  638. for (let i = 0; i < this._delayedClicks.length; i++) {
  639. if (this._delayedClicks[i]) {
  640. // If the button that was pressed is the same as the one that was released,
  641. // just clear the timer. This will be resolved in the up event.
  642. if (evt.button === i) {
  643. clearTimeout(this._delayedClicks[i]?.timeoutId);
  644. }
  645. else {
  646. // Otherwise, we need to resolve the click
  647. const clickInfo = this._delayedClicks[i].clickInfo;
  648. this._doubleClickOccured = false;
  649. clickInfo.singleClick = true;
  650. clickInfo.ignore = false;
  651. const prevEvt = this._delayedClicks[i].evt;
  652. const type = PointerEventTypes.POINTERTAP;
  653. const pi = new PointerInfo(type, prevEvt, this._currentPickResult);
  654. if (scene.onPointerObservable.hasObservers() && scene.onPointerObservable.hasSpecificMask(type)) {
  655. scene.onPointerObservable.notifyObservers(pi, type);
  656. }
  657. // Clear the delayed click
  658. this._delayedClicks[i] = null;
  659. }
  660. }
  661. }
  662. }
  663. this._updatePointerPosition(evt);
  664. if (this._swipeButtonPressed === -1) {
  665. this._swipeButtonPressed = evt.button;
  666. }
  667. if (scene.preventDefaultOnPointerDown && elementToAttachTo) {
  668. evt.preventDefault();
  669. elementToAttachTo.focus();
  670. }
  671. this._startingPointerPosition.x = this._pointerX;
  672. this._startingPointerPosition.y = this._pointerY;
  673. this._startingPointerTime = Date.now();
  674. // PreObservable support
  675. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOWN)) {
  676. return;
  677. }
  678. if (!scene.cameraToUseForPointers && !scene.activeCamera) {
  679. return;
  680. }
  681. this._pointerCaptures[evt.pointerId] = true;
  682. if (!scene.pointerDownPredicate) {
  683. scene.pointerDownPredicate = (mesh) => {
  684. return (mesh.isPickable &&
  685. mesh.isVisible &&
  686. mesh.isReady() &&
  687. mesh.isEnabled() &&
  688. (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0));
  689. };
  690. }
  691. // Meshes
  692. this._pickedDownMesh = null;
  693. let pickResult;
  694. if (scene.skipPointerDownPicking || (scene._registeredActions === 0 && !this._checkForPicking() && !scene.onPointerDown)) {
  695. pickResult = new PickingInfo();
  696. }
  697. else {
  698. pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerDownPredicate, scene.pointerDownFastCheck, scene.cameraToUseForPointers, scene.pointerDownTrianglePredicate);
  699. }
  700. this._processPointerDown(pickResult, evt);
  701. };
  702. this._onPointerUp = (evt) => {
  703. if (this._totalPointersPressed === 0) {
  704. // We are attaching the pointer up to windows because of a bug in FF
  705. return; // So we need to test it the pointer down was pressed before.
  706. }
  707. this._totalPointersPressed--;
  708. this._pickedUpMesh = null;
  709. this._meshPickProceed = false;
  710. this._updatePointerPosition(evt);
  711. if (scene.preventDefaultOnPointerUp && elementToAttachTo) {
  712. evt.preventDefault();
  713. elementToAttachTo.focus();
  714. }
  715. this._initClickEvent(scene.onPrePointerObservable, scene.onPointerObservable, evt, (clickInfo, pickResult) => {
  716. // PreObservable support
  717. if (scene.onPrePointerObservable.hasObservers()) {
  718. this._skipPointerTap = false;
  719. if (!clickInfo.ignore) {
  720. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERUP)) {
  721. // If we're skipping the next observable, we need to reset the swipe state before returning
  722. if (this._swipeButtonPressed === evt.button) {
  723. this._isSwiping = false;
  724. this._swipeButtonPressed = -1;
  725. }
  726. // If we're going to skip the POINTERUP, we need to reset the pointer capture
  727. if (evt.buttons === 0) {
  728. this._pointerCaptures[evt.pointerId] = false;
  729. }
  730. return;
  731. }
  732. if (!clickInfo.hasSwiped) {
  733. if (clickInfo.singleClick && scene.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERTAP)) {
  734. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERTAP)) {
  735. this._skipPointerTap = true;
  736. }
  737. }
  738. if (clickInfo.doubleClick && scene.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP)) {
  739. if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOUBLETAP)) {
  740. this._skipPointerTap = true;
  741. }
  742. }
  743. }
  744. }
  745. }
  746. // There should be a pointer captured at this point so if there isn't we should reset and return
  747. if (!this._pointerCaptures[evt.pointerId]) {
  748. if (this._swipeButtonPressed === evt.button) {
  749. this._isSwiping = false;
  750. this._swipeButtonPressed = -1;
  751. }
  752. return;
  753. }
  754. // Only release capture if all buttons are released
  755. if (evt.buttons === 0) {
  756. this._pointerCaptures[evt.pointerId] = false;
  757. }
  758. if (!scene.cameraToUseForPointers && !scene.activeCamera) {
  759. return;
  760. }
  761. if (!scene.pointerUpPredicate) {
  762. scene.pointerUpPredicate = (mesh) => {
  763. return (mesh.isPickable &&
  764. mesh.isVisible &&
  765. mesh.isReady() &&
  766. mesh.isEnabled() &&
  767. (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0));
  768. };
  769. }
  770. // Meshes
  771. if (!this._meshPickProceed && ((AbstractActionManager && AbstractActionManager.HasTriggers) || this._checkForPicking() || scene.onPointerUp)) {
  772. this._initActionManager(null, clickInfo);
  773. }
  774. if (!pickResult) {
  775. pickResult = this._currentPickResult;
  776. }
  777. this._processPointerUp(pickResult, evt, clickInfo);
  778. this._previousPickResult = this._currentPickResult;
  779. if (this._swipeButtonPressed === evt.button) {
  780. this._isSwiping = false;
  781. this._swipeButtonPressed = -1;
  782. }
  783. });
  784. };
  785. this._onKeyDown = (evt) => {
  786. const type = KeyboardEventTypes.KEYDOWN;
  787. if (scene.onPreKeyboardObservable.hasObservers()) {
  788. const pi = new KeyboardInfoPre(type, evt);
  789. scene.onPreKeyboardObservable.notifyObservers(pi, type);
  790. if (pi.skipOnKeyboardObservable) {
  791. return;
  792. }
  793. }
  794. if (scene.onKeyboardObservable.hasObservers()) {
  795. const pi = new KeyboardInfo(type, evt);
  796. scene.onKeyboardObservable.notifyObservers(pi, type);
  797. }
  798. if (scene.actionManager) {
  799. scene.actionManager.processTrigger(14, ActionEvent.CreateNewFromScene(scene, evt));
  800. }
  801. };
  802. this._onKeyUp = (evt) => {
  803. const type = KeyboardEventTypes.KEYUP;
  804. if (scene.onPreKeyboardObservable.hasObservers()) {
  805. const pi = new KeyboardInfoPre(type, evt);
  806. scene.onPreKeyboardObservable.notifyObservers(pi, type);
  807. if (pi.skipOnKeyboardObservable) {
  808. return;
  809. }
  810. }
  811. if (scene.onKeyboardObservable.hasObservers()) {
  812. const pi = new KeyboardInfo(type, evt);
  813. scene.onKeyboardObservable.notifyObservers(pi, type);
  814. }
  815. if (scene.actionManager) {
  816. scene.actionManager.processTrigger(15, ActionEvent.CreateNewFromScene(scene, evt));
  817. }
  818. };
  819. // If a device connects that we can handle, wire up the observable
  820. this._deviceSourceManager.onDeviceConnectedObservable.add((deviceSource) => {
  821. if (deviceSource.deviceType === DeviceType.Mouse) {
  822. deviceSource.onInputChangedObservable.add((eventData) => {
  823. if (eventData.inputIndex === PointerInput.LeftClick ||
  824. eventData.inputIndex === PointerInput.MiddleClick ||
  825. eventData.inputIndex === PointerInput.RightClick ||
  826. eventData.inputIndex === PointerInput.BrowserBack ||
  827. eventData.inputIndex === PointerInput.BrowserForward) {
  828. if (attachDown && deviceSource.getInput(eventData.inputIndex) === 1) {
  829. this._onPointerDown(eventData);
  830. }
  831. else if (attachUp && deviceSource.getInput(eventData.inputIndex) === 0) {
  832. this._onPointerUp(eventData);
  833. }
  834. }
  835. else if (attachMove) {
  836. if (eventData.inputIndex === PointerInput.Move) {
  837. this._onPointerMove(eventData);
  838. }
  839. else if (eventData.inputIndex === PointerInput.MouseWheelX ||
  840. eventData.inputIndex === PointerInput.MouseWheelY ||
  841. eventData.inputIndex === PointerInput.MouseWheelZ) {
  842. this._onPointerMove(eventData);
  843. }
  844. }
  845. });
  846. }
  847. else if (deviceSource.deviceType === DeviceType.Touch) {
  848. deviceSource.onInputChangedObservable.add((eventData) => {
  849. if (eventData.inputIndex === PointerInput.LeftClick) {
  850. if (attachDown && deviceSource.getInput(eventData.inputIndex) === 1) {
  851. this._onPointerDown(eventData);
  852. if (this._totalPointersPressed > 1) {
  853. this._isMultiTouchGesture = true;
  854. }
  855. }
  856. else if (attachUp && deviceSource.getInput(eventData.inputIndex) === 0) {
  857. this._onPointerUp(eventData);
  858. if (this._totalPointersPressed === 0) {
  859. this._isMultiTouchGesture = false;
  860. }
  861. }
  862. }
  863. if (attachMove && eventData.inputIndex === PointerInput.Move) {
  864. this._onPointerMove(eventData);
  865. }
  866. });
  867. }
  868. else if (deviceSource.deviceType === DeviceType.Keyboard) {
  869. deviceSource.onInputChangedObservable.add((eventData) => {
  870. if (eventData.type === "keydown") {
  871. this._onKeyDown(eventData);
  872. }
  873. else if (eventData.type === "keyup") {
  874. this._onKeyUp(eventData);
  875. }
  876. });
  877. }
  878. });
  879. this._alreadyAttached = true;
  880. }
  881. /**
  882. * Detaches all event handlers
  883. */
  884. detachControl() {
  885. if (this._alreadyAttached) {
  886. this._deviceSourceManager.dispose();
  887. this._deviceSourceManager = null;
  888. // Cursor
  889. if (this._alreadyAttachedTo && !this._scene.doNotHandleCursors) {
  890. this._alreadyAttachedTo.style.cursor = this._scene.defaultCursor;
  891. }
  892. this._alreadyAttached = false;
  893. this._alreadyAttachedTo = null;
  894. }
  895. }
  896. /**
  897. * Force the value of meshUnderPointer
  898. * @param mesh - defines the mesh to use
  899. * @param pointerId - optional pointer id when using more than one pointer. Defaults to 0
  900. * @param pickResult - optional pickingInfo data used to find mesh
  901. * @param evt - optional pointer event
  902. */
  903. setPointerOverMesh(mesh, pointerId = 0, pickResult, evt) {
  904. if (this._meshUnderPointerId[pointerId] === mesh && (!mesh || !mesh._internalAbstractMeshDataInfo._pointerOverDisableMeshTesting)) {
  905. return;
  906. }
  907. const underPointerMesh = this._meshUnderPointerId[pointerId];
  908. let actionManager;
  909. if (underPointerMesh) {
  910. actionManager = underPointerMesh._getActionManagerForTrigger(10);
  911. if (actionManager) {
  912. actionManager.processTrigger(10, ActionEvent.CreateNew(underPointerMesh, evt, { pointerId }));
  913. }
  914. }
  915. if (mesh) {
  916. this._meshUnderPointerId[pointerId] = mesh;
  917. this._pointerOverMesh = mesh;
  918. actionManager = mesh._getActionManagerForTrigger(9);
  919. if (actionManager) {
  920. actionManager.processTrigger(9, ActionEvent.CreateNew(mesh, evt, { pointerId, pickResult }));
  921. }
  922. }
  923. else {
  924. delete this._meshUnderPointerId[pointerId];
  925. this._pointerOverMesh = null;
  926. }
  927. }
  928. /**
  929. * Gets the mesh under the pointer
  930. * @returns a Mesh or null if no mesh is under the pointer
  931. */
  932. getPointerOverMesh() {
  933. return this.meshUnderPointer;
  934. }
  935. /**
  936. * @param mesh - Mesh to invalidate
  937. * @internal
  938. */
  939. _invalidateMesh(mesh) {
  940. if (this._pointerOverMesh === mesh) {
  941. this._pointerOverMesh = null;
  942. }
  943. if (this._pickedDownMesh === mesh) {
  944. this._pickedDownMesh = null;
  945. }
  946. if (this._pickedUpMesh === mesh) {
  947. this._pickedUpMesh = null;
  948. }
  949. for (const pointerId in this._meshUnderPointerId) {
  950. if (this._meshUnderPointerId[pointerId] === mesh) {
  951. delete this._meshUnderPointerId[pointerId];
  952. }
  953. }
  954. }
  955. }
  956. /** The distance in pixel that you have to move to prevent some events */
  957. InputManager.DragMovementThreshold = 10; // in pixels
  958. /** Time in milliseconds to wait to raise long press events if button is still pressed */
  959. InputManager.LongPressDelay = 500; // in milliseconds
  960. /** Time in milliseconds with two consecutive clicks will be considered as a double click */
  961. InputManager.DoubleClickDelay = 300; // in milliseconds
  962. /**
  963. * This flag will modify the behavior so that, when true, a click will happen if and only if
  964. * another click DOES NOT happen within the DoubleClickDelay time frame. If another click does
  965. * happen within that time frame, the first click will not fire an event and and a double click will occur.
  966. */
  967. InputManager.ExclusiveDoubleClickMode = false;
  968. //# sourceMappingURL=scene.inputManager.js.map