webDeviceInputSystem.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. import { IsNavigatorAvailable } from "../Misc/domManagement.js";
  2. import { Tools } from "../Misc/tools.js";
  3. import { DeviceEventFactory } from "./eventFactory.js";
  4. import { DeviceType, PointerInput } from "./InputDevices/deviceEnums.js";
  5. // eslint-disable-next-line @typescript-eslint/naming-convention
  6. const MAX_KEYCODES = 255;
  7. // eslint-disable-next-line @typescript-eslint/naming-convention
  8. const MAX_POINTER_INPUTS = Object.keys(PointerInput).length / 2;
  9. /** @internal */
  10. export class WebDeviceInputSystem {
  11. /**
  12. * Constructor for the WebDeviceInputSystem
  13. * @param engine Engine to reference
  14. * @param onDeviceConnected Callback to execute when device is connected
  15. * @param onDeviceDisconnected Callback to execute when device is disconnected
  16. * @param onInputChanged Callback to execute when input changes on device
  17. */
  18. constructor(engine, onDeviceConnected, onDeviceDisconnected, onInputChanged) {
  19. // Private Members
  20. this._inputs = [];
  21. this._keyboardActive = false;
  22. this._pointerActive = false;
  23. this._usingSafari = Tools.IsSafari();
  24. // Found solution for determining if MacOS is being used here:
  25. // https://stackoverflow.com/questions/10527983/best-way-to-detect-mac-os-x-or-windows-computers-with-javascript-or-jquery
  26. this._usingMacOS = IsNavigatorAvailable() && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
  27. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  28. this._keyboardDownEvent = (evt) => { };
  29. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  30. this._keyboardUpEvent = (evt) => { };
  31. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  32. this._keyboardBlurEvent = (evt) => { };
  33. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  34. this._pointerMoveEvent = (evt) => { };
  35. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  36. this._pointerDownEvent = (evt) => { };
  37. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  38. this._pointerUpEvent = (evt) => { };
  39. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  40. this._pointerCancelEvent = (evt) => { };
  41. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  42. this._pointerWheelEvent = (evt) => { };
  43. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  44. this._pointerBlurEvent = (evt) => { };
  45. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  46. this._pointerMacOSChromeOutEvent = (evt) => { };
  47. this._eventsAttached = false;
  48. this._mouseId = -1;
  49. this._isUsingFirefox = IsNavigatorAvailable() && navigator.userAgent && navigator.userAgent.indexOf("Firefox") !== -1;
  50. this._isUsingChromium = IsNavigatorAvailable() && navigator.userAgent && navigator.userAgent.indexOf("Chrome") !== -1;
  51. this._maxTouchPoints = 0;
  52. this._pointerInputClearObserver = null;
  53. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  54. this._gamepadConnectedEvent = (evt) => { };
  55. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  56. this._gamepadDisconnectedEvent = (evt) => { };
  57. this._eventPrefix = Tools.GetPointerPrefix(engine);
  58. this._engine = engine;
  59. this._onDeviceConnected = onDeviceConnected;
  60. this._onDeviceDisconnected = onDeviceDisconnected;
  61. this._onInputChanged = onInputChanged;
  62. // If we need a pointerId, set one for future use
  63. this._mouseId = this._isUsingFirefox ? 0 : 1;
  64. this._enableEvents();
  65. if (this._usingMacOS) {
  66. this._metaKeys = [];
  67. }
  68. // Set callback to enable event handler switching when inputElement changes
  69. if (!this._engine._onEngineViewChanged) {
  70. this._engine._onEngineViewChanged = () => {
  71. this._enableEvents();
  72. };
  73. }
  74. }
  75. // Public functions
  76. /**
  77. * Checks for current device input value, given an id and input index. Throws exception if requested device not initialized.
  78. * @param deviceType Enum specifying device type
  79. * @param deviceSlot "Slot" or index that device is referenced in
  80. * @param inputIndex Id of input to be checked
  81. * @returns Current value of input
  82. */
  83. pollInput(deviceType, deviceSlot, inputIndex) {
  84. const device = this._inputs[deviceType][deviceSlot];
  85. if (!device) {
  86. // eslint-disable-next-line no-throw-literal
  87. throw `Unable to find device ${DeviceType[deviceType]}`;
  88. }
  89. if (deviceType >= DeviceType.DualShock && deviceType <= DeviceType.DualSense) {
  90. this._updateDevice(deviceType, deviceSlot, inputIndex);
  91. }
  92. const currentValue = device[inputIndex];
  93. if (currentValue === undefined) {
  94. // eslint-disable-next-line no-throw-literal
  95. throw `Unable to find input ${inputIndex} for device ${DeviceType[deviceType]} in slot ${deviceSlot}`;
  96. }
  97. if (inputIndex === PointerInput.Move) {
  98. Tools.Warn(`Unable to provide information for PointerInput.Move. Try using PointerInput.Horizontal or PointerInput.Vertical for move data.`);
  99. }
  100. return currentValue;
  101. }
  102. /**
  103. * Check for a specific device in the DeviceInputSystem
  104. * @param deviceType Type of device to check for
  105. * @returns bool with status of device's existence
  106. */
  107. isDeviceAvailable(deviceType) {
  108. return this._inputs[deviceType] !== undefined;
  109. }
  110. /**
  111. * Dispose of all the eventlisteners
  112. */
  113. dispose() {
  114. // Callbacks
  115. this._onDeviceConnected = () => { };
  116. this._onDeviceDisconnected = () => { };
  117. this._onInputChanged = () => { };
  118. delete this._engine._onEngineViewChanged;
  119. if (this._elementToAttachTo) {
  120. this._disableEvents();
  121. }
  122. }
  123. /**
  124. * Enable listening for user input events
  125. */
  126. _enableEvents() {
  127. const inputElement = this?._engine.getInputElement();
  128. if (inputElement && (!this._eventsAttached || this._elementToAttachTo !== inputElement)) {
  129. // Remove events before adding to avoid double events or simultaneous events on multiple canvases
  130. this._disableEvents();
  131. // If the inputs array has already been created, zero it out to before setting up events
  132. if (this._inputs) {
  133. for (const inputs of this._inputs) {
  134. if (inputs) {
  135. for (const deviceSlotKey in inputs) {
  136. const deviceSlot = +deviceSlotKey;
  137. const device = inputs[deviceSlot];
  138. if (device) {
  139. for (let inputIndex = 0; inputIndex < device.length; inputIndex++) {
  140. device[inputIndex] = 0;
  141. }
  142. }
  143. }
  144. }
  145. }
  146. }
  147. this._elementToAttachTo = inputElement;
  148. // Set tab index for the inputElement to the engine's canvasTabIndex, if and only if the element's tab index is -1
  149. this._elementToAttachTo.tabIndex = this._elementToAttachTo.tabIndex !== -1 ? this._elementToAttachTo.tabIndex : this._engine.canvasTabIndex;
  150. this._handleKeyActions();
  151. this._handlePointerActions();
  152. this._handleGamepadActions();
  153. this._eventsAttached = true;
  154. // Check for devices that are already connected but aren't registered. Currently, only checks for gamepads and mouse
  155. this._checkForConnectedDevices();
  156. }
  157. }
  158. /**
  159. * Disable listening for user input events
  160. */
  161. _disableEvents() {
  162. if (this._elementToAttachTo) {
  163. // Blur Events
  164. this._elementToAttachTo.removeEventListener("blur", this._keyboardBlurEvent);
  165. this._elementToAttachTo.removeEventListener("blur", this._pointerBlurEvent);
  166. // Keyboard Events
  167. this._elementToAttachTo.removeEventListener("keydown", this._keyboardDownEvent);
  168. this._elementToAttachTo.removeEventListener("keyup", this._keyboardUpEvent);
  169. // Pointer Events
  170. this._elementToAttachTo.removeEventListener(this._eventPrefix + "move", this._pointerMoveEvent);
  171. this._elementToAttachTo.removeEventListener(this._eventPrefix + "down", this._pointerDownEvent);
  172. this._elementToAttachTo.removeEventListener(this._eventPrefix + "up", this._pointerUpEvent);
  173. this._elementToAttachTo.removeEventListener(this._eventPrefix + "cancel", this._pointerCancelEvent);
  174. this._elementToAttachTo.removeEventListener(this._wheelEventName, this._pointerWheelEvent);
  175. if (this._usingMacOS && this._isUsingChromium) {
  176. this._elementToAttachTo.removeEventListener("lostpointercapture", this._pointerMacOSChromeOutEvent);
  177. }
  178. // Gamepad Events
  179. window.removeEventListener("gamepadconnected", this._gamepadConnectedEvent);
  180. window.removeEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
  181. }
  182. if (this._pointerInputClearObserver) {
  183. this._engine.onEndFrameObservable.remove(this._pointerInputClearObserver);
  184. }
  185. this._eventsAttached = false;
  186. }
  187. /**
  188. * Checks for existing connections to devices and register them, if necessary
  189. * Currently handles gamepads and mouse
  190. */
  191. _checkForConnectedDevices() {
  192. if (navigator.getGamepads) {
  193. const gamepads = navigator.getGamepads();
  194. for (const gamepad of gamepads) {
  195. if (gamepad) {
  196. this._addGamePad(gamepad);
  197. }
  198. }
  199. }
  200. // If the device in use has mouse capabilities, pre-register mouse
  201. if (typeof matchMedia === "function" && matchMedia("(pointer:fine)").matches) {
  202. // This will provide a dummy value for the cursor position and is expected to be overridden when the first mouse event happens.
  203. // There isn't any good way to get the current position outside of a pointer event so that's why this was done.
  204. this._addPointerDevice(DeviceType.Mouse, 0, 0, 0);
  205. }
  206. }
  207. // Private functions
  208. /**
  209. * Add a gamepad to the DeviceInputSystem
  210. * @param gamepad A single DOM Gamepad object
  211. */
  212. _addGamePad(gamepad) {
  213. const deviceType = this._getGamepadDeviceType(gamepad.id);
  214. const deviceSlot = gamepad.index;
  215. this._gamepads = this._gamepads || new Array(gamepad.index + 1);
  216. this._registerDevice(deviceType, deviceSlot, gamepad.buttons.length + gamepad.axes.length);
  217. this._gamepads[deviceSlot] = deviceType;
  218. }
  219. /**
  220. * Add pointer device to DeviceInputSystem
  221. * @param deviceType Type of Pointer to add
  222. * @param deviceSlot Pointer ID (0 for mouse, pointerId for Touch)
  223. * @param currentX Current X at point of adding
  224. * @param currentY Current Y at point of adding
  225. */
  226. _addPointerDevice(deviceType, deviceSlot, currentX, currentY) {
  227. if (!this._pointerActive) {
  228. this._pointerActive = true;
  229. }
  230. this._registerDevice(deviceType, deviceSlot, MAX_POINTER_INPUTS);
  231. const pointer = this._inputs[deviceType][deviceSlot]; /* initialize our pointer position immediately after registration */
  232. pointer[0] = currentX;
  233. pointer[1] = currentY;
  234. }
  235. /**
  236. * Add device and inputs to device array
  237. * @param deviceType Enum specifying device type
  238. * @param deviceSlot "Slot" or index that device is referenced in
  239. * @param numberOfInputs Number of input entries to create for given device
  240. */
  241. _registerDevice(deviceType, deviceSlot, numberOfInputs) {
  242. if (deviceSlot === undefined) {
  243. // eslint-disable-next-line no-throw-literal
  244. throw `Unable to register device ${DeviceType[deviceType]} to undefined slot.`;
  245. }
  246. if (!this._inputs[deviceType]) {
  247. this._inputs[deviceType] = {};
  248. }
  249. if (!this._inputs[deviceType][deviceSlot]) {
  250. const device = new Array(numberOfInputs);
  251. device.fill(0);
  252. this._inputs[deviceType][deviceSlot] = device;
  253. this._onDeviceConnected(deviceType, deviceSlot);
  254. }
  255. }
  256. /**
  257. * Given a specific device name, remove that device from the device map
  258. * @param deviceType Enum specifying device type
  259. * @param deviceSlot "Slot" or index that device is referenced in
  260. */
  261. _unregisterDevice(deviceType, deviceSlot) {
  262. if (this._inputs[deviceType][deviceSlot]) {
  263. delete this._inputs[deviceType][deviceSlot];
  264. this._onDeviceDisconnected(deviceType, deviceSlot);
  265. }
  266. }
  267. /**
  268. * Handle all actions that come from keyboard interaction
  269. */
  270. _handleKeyActions() {
  271. this._keyboardDownEvent = (evt) => {
  272. if (!this._keyboardActive) {
  273. this._keyboardActive = true;
  274. this._registerDevice(DeviceType.Keyboard, 0, MAX_KEYCODES);
  275. }
  276. const kbKey = this._inputs[DeviceType.Keyboard][0];
  277. if (kbKey) {
  278. kbKey[evt.keyCode] = 1;
  279. const deviceEvent = evt;
  280. deviceEvent.inputIndex = evt.keyCode;
  281. if (this._usingMacOS && evt.metaKey && evt.key !== "Meta") {
  282. if (!this._metaKeys.includes(evt.keyCode)) {
  283. this._metaKeys.push(evt.keyCode);
  284. }
  285. }
  286. this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
  287. }
  288. };
  289. this._keyboardUpEvent = (evt) => {
  290. if (!this._keyboardActive) {
  291. this._keyboardActive = true;
  292. this._registerDevice(DeviceType.Keyboard, 0, MAX_KEYCODES);
  293. }
  294. const kbKey = this._inputs[DeviceType.Keyboard][0];
  295. if (kbKey) {
  296. kbKey[evt.keyCode] = 0;
  297. const deviceEvent = evt;
  298. deviceEvent.inputIndex = evt.keyCode;
  299. if (this._usingMacOS && evt.key === "Meta" && this._metaKeys.length > 0) {
  300. for (const keyCode of this._metaKeys) {
  301. const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Keyboard, 0, keyCode, 0, this, this._elementToAttachTo);
  302. kbKey[keyCode] = 0;
  303. this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
  304. }
  305. this._metaKeys.splice(0, this._metaKeys.length);
  306. }
  307. this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
  308. }
  309. };
  310. this._keyboardBlurEvent = () => {
  311. if (this._keyboardActive) {
  312. const kbKey = this._inputs[DeviceType.Keyboard][0];
  313. for (let i = 0; i < kbKey.length; i++) {
  314. if (kbKey[i] !== 0) {
  315. kbKey[i] = 0;
  316. const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Keyboard, 0, i, 0, this, this._elementToAttachTo);
  317. this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
  318. }
  319. }
  320. if (this._usingMacOS) {
  321. this._metaKeys.splice(0, this._metaKeys.length);
  322. }
  323. }
  324. };
  325. this._elementToAttachTo.addEventListener("keydown", this._keyboardDownEvent);
  326. this._elementToAttachTo.addEventListener("keyup", this._keyboardUpEvent);
  327. this._elementToAttachTo.addEventListener("blur", this._keyboardBlurEvent);
  328. }
  329. /**
  330. * Handle all actions that come from pointer interaction
  331. */
  332. _handlePointerActions() {
  333. // If maxTouchPoints is defined, use that value. Otherwise, allow for a minimum for supported gestures like pinch
  334. this._maxTouchPoints = (IsNavigatorAvailable() && navigator.maxTouchPoints) || 2;
  335. if (!this._activeTouchIds) {
  336. this._activeTouchIds = new Array(this._maxTouchPoints);
  337. }
  338. for (let i = 0; i < this._maxTouchPoints; i++) {
  339. this._activeTouchIds[i] = -1;
  340. }
  341. this._pointerMoveEvent = (evt) => {
  342. const deviceType = this._getPointerType(evt);
  343. let deviceSlot = deviceType === DeviceType.Mouse ? 0 : this._activeTouchIds.indexOf(evt.pointerId);
  344. // In the event that we're gettting pointermove events from touch inputs that we aren't tracking,
  345. // look for an available slot and retroactively connect it.
  346. if (deviceType === DeviceType.Touch && deviceSlot === -1) {
  347. const idx = this._activeTouchIds.indexOf(-1);
  348. if (idx >= 0) {
  349. deviceSlot = idx;
  350. this._activeTouchIds[idx] = evt.pointerId;
  351. // Because this is a "new" input, inform the connected callback
  352. this._onDeviceConnected(deviceType, deviceSlot);
  353. }
  354. else {
  355. // We can't find an open slot to store new pointer so just return (can only support max number of touches)
  356. Tools.Warn(`Max number of touches exceeded. Ignoring touches in excess of ${this._maxTouchPoints}`);
  357. return;
  358. }
  359. }
  360. if (!this._inputs[deviceType]) {
  361. this._inputs[deviceType] = {};
  362. }
  363. if (!this._inputs[deviceType][deviceSlot]) {
  364. this._addPointerDevice(deviceType, deviceSlot, evt.clientX, evt.clientY);
  365. }
  366. const pointer = this._inputs[deviceType][deviceSlot];
  367. if (pointer) {
  368. const deviceEvent = evt;
  369. deviceEvent.inputIndex = PointerInput.Move;
  370. pointer[PointerInput.Horizontal] = evt.clientX;
  371. pointer[PointerInput.Vertical] = evt.clientY;
  372. // For touches that aren't started with a down, we need to set the button state to 1
  373. if (deviceType === DeviceType.Touch && pointer[PointerInput.LeftClick] === 0) {
  374. pointer[PointerInput.LeftClick] = 1;
  375. }
  376. if (evt.pointerId === undefined) {
  377. evt.pointerId = this._mouseId;
  378. }
  379. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  380. // Lets Propagate the event for move with same position.
  381. if (!this._usingSafari && evt.button !== -1) {
  382. deviceEvent.inputIndex = evt.button + 2;
  383. pointer[evt.button + 2] = pointer[evt.button + 2] ? 0 : 1; // Reverse state of button if evt.button has value
  384. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  385. }
  386. }
  387. };
  388. this._pointerDownEvent = (evt) => {
  389. const deviceType = this._getPointerType(evt);
  390. let deviceSlot = deviceType === DeviceType.Mouse ? 0 : evt.pointerId;
  391. if (deviceType === DeviceType.Touch) {
  392. const idx = this._activeTouchIds.indexOf(-1);
  393. if (idx >= 0) {
  394. deviceSlot = idx;
  395. this._activeTouchIds[idx] = evt.pointerId;
  396. }
  397. else {
  398. // We can't find an open slot to store new pointer so just return (can only support max number of touches)
  399. Tools.Warn(`Max number of touches exceeded. Ignoring touches in excess of ${this._maxTouchPoints}`);
  400. return;
  401. }
  402. }
  403. if (!this._inputs[deviceType]) {
  404. this._inputs[deviceType] = {};
  405. }
  406. if (!this._inputs[deviceType][deviceSlot]) {
  407. this._addPointerDevice(deviceType, deviceSlot, evt.clientX, evt.clientY);
  408. }
  409. else if (deviceType === DeviceType.Touch) {
  410. this._onDeviceConnected(deviceType, deviceSlot);
  411. }
  412. const pointer = this._inputs[deviceType][deviceSlot];
  413. if (pointer) {
  414. const previousHorizontal = pointer[PointerInput.Horizontal];
  415. const previousVertical = pointer[PointerInput.Vertical];
  416. if (deviceType === DeviceType.Mouse) {
  417. // Mouse; Set pointerId if undefined
  418. if (evt.pointerId === undefined) {
  419. evt.pointerId = this._mouseId;
  420. }
  421. if (!document.pointerLockElement) {
  422. try {
  423. this._elementToAttachTo.setPointerCapture(this._mouseId);
  424. }
  425. catch (e) {
  426. // DO NOTHING
  427. }
  428. }
  429. }
  430. else {
  431. // Touch; Since touches are dynamically assigned, only set capture if we have an id
  432. if (evt.pointerId && !document.pointerLockElement) {
  433. try {
  434. this._elementToAttachTo.setPointerCapture(evt.pointerId);
  435. }
  436. catch (e) {
  437. // DO NOTHING
  438. }
  439. }
  440. }
  441. pointer[PointerInput.Horizontal] = evt.clientX;
  442. pointer[PointerInput.Vertical] = evt.clientY;
  443. pointer[evt.button + 2] = 1;
  444. const deviceEvent = evt;
  445. // NOTE: The +2 used here to is because PointerInput has the same value progression for its mouse buttons as PointerEvent.button
  446. // However, we have our X and Y values front-loaded to group together the touch inputs but not break this progression
  447. // EG. ([X, Y, Left-click], Middle-click, etc...)
  448. deviceEvent.inputIndex = evt.button + 2;
  449. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  450. if (previousHorizontal !== evt.clientX || previousVertical !== evt.clientY) {
  451. deviceEvent.inputIndex = PointerInput.Move;
  452. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  453. }
  454. }
  455. };
  456. this._pointerUpEvent = (evt) => {
  457. const deviceType = this._getPointerType(evt);
  458. const deviceSlot = deviceType === DeviceType.Mouse ? 0 : this._activeTouchIds.indexOf(evt.pointerId);
  459. if (deviceType === DeviceType.Touch) {
  460. // If we're getting a pointerup event for a touch that isn't active, just return.
  461. if (deviceSlot === -1) {
  462. return;
  463. }
  464. else {
  465. this._activeTouchIds[deviceSlot] = -1;
  466. }
  467. }
  468. const pointer = this._inputs[deviceType]?.[deviceSlot];
  469. if (pointer && pointer[evt.button + 2] !== 0) {
  470. const previousHorizontal = pointer[PointerInput.Horizontal];
  471. const previousVertical = pointer[PointerInput.Vertical];
  472. pointer[PointerInput.Horizontal] = evt.clientX;
  473. pointer[PointerInput.Vertical] = evt.clientY;
  474. pointer[evt.button + 2] = 0;
  475. const deviceEvent = evt;
  476. if (evt.pointerId === undefined) {
  477. evt.pointerId = this._mouseId;
  478. }
  479. if (previousHorizontal !== evt.clientX || previousVertical !== evt.clientY) {
  480. deviceEvent.inputIndex = PointerInput.Move;
  481. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  482. }
  483. // NOTE: The +2 used here to is because PointerInput has the same value progression for its mouse buttons as PointerEvent.button
  484. // However, we have our X and Y values front-loaded to group together the touch inputs but not break this progression
  485. // EG. ([X, Y, Left-click], Middle-click, etc...)
  486. deviceEvent.inputIndex = evt.button + 2;
  487. if (deviceType === DeviceType.Mouse && this._mouseId >= 0 && this._elementToAttachTo.hasPointerCapture?.(this._mouseId)) {
  488. this._elementToAttachTo.releasePointerCapture(this._mouseId);
  489. }
  490. else if (evt.pointerId && this._elementToAttachTo.hasPointerCapture?.(evt.pointerId)) {
  491. this._elementToAttachTo.releasePointerCapture(evt.pointerId);
  492. }
  493. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  494. if (deviceType === DeviceType.Touch) {
  495. this._onDeviceDisconnected(deviceType, deviceSlot);
  496. }
  497. }
  498. };
  499. this._pointerCancelEvent = (evt) => {
  500. if (evt.pointerType === "mouse") {
  501. const pointer = this._inputs[DeviceType.Mouse][0];
  502. if (this._mouseId >= 0 && this._elementToAttachTo.hasPointerCapture?.(this._mouseId)) {
  503. this._elementToAttachTo.releasePointerCapture(this._mouseId);
  504. }
  505. for (let inputIndex = PointerInput.LeftClick; inputIndex <= PointerInput.BrowserForward; inputIndex++) {
  506. if (pointer[inputIndex] === 1) {
  507. pointer[inputIndex] = 0;
  508. const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Mouse, 0, inputIndex, 0, this, this._elementToAttachTo);
  509. this._onInputChanged(DeviceType.Mouse, 0, deviceEvent);
  510. }
  511. }
  512. }
  513. else {
  514. const deviceSlot = this._activeTouchIds.indexOf(evt.pointerId);
  515. // If we're getting a pointercancel event for a touch that isn't active, just return
  516. if (deviceSlot === -1) {
  517. return;
  518. }
  519. if (this._elementToAttachTo.hasPointerCapture?.(evt.pointerId)) {
  520. this._elementToAttachTo.releasePointerCapture(evt.pointerId);
  521. }
  522. this._inputs[DeviceType.Touch][deviceSlot][PointerInput.LeftClick] = 0;
  523. const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Touch, deviceSlot, PointerInput.LeftClick, 0, this, this._elementToAttachTo, evt.pointerId);
  524. this._onInputChanged(DeviceType.Touch, deviceSlot, deviceEvent);
  525. this._activeTouchIds[deviceSlot] = -1;
  526. this._onDeviceDisconnected(DeviceType.Touch, deviceSlot);
  527. }
  528. };
  529. // Set Wheel Event Name, code originally from scene.inputManager
  530. this._wheelEventName =
  531. "onwheel" in document.createElement("div")
  532. ? "wheel" // Modern browsers support "wheel"
  533. : document.onmousewheel !== undefined
  534. ? "mousewheel" // Webkit and IE support at least "mousewheel"
  535. : "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox
  536. // Code originally in scene.inputManager.ts
  537. // Chrome reports warning in console if wheel listener doesn't set an explicit passive option.
  538. // IE11 only supports captureEvent:boolean, not options:object, and it defaults to false.
  539. // Feature detection technique copied from: https://github.com/github/eventlistener-polyfill (MIT license)
  540. let passiveSupported = false;
  541. const noop = function () { };
  542. try {
  543. const options = Object.defineProperty({}, "passive", {
  544. get: function () {
  545. passiveSupported = true;
  546. },
  547. });
  548. this._elementToAttachTo.addEventListener("test", noop, options);
  549. this._elementToAttachTo.removeEventListener("test", noop, options);
  550. }
  551. catch (e) {
  552. /* */
  553. }
  554. this._pointerBlurEvent = () => {
  555. // Handle mouse buttons
  556. if (this.isDeviceAvailable(DeviceType.Mouse)) {
  557. const pointer = this._inputs[DeviceType.Mouse][0];
  558. if (this._mouseId >= 0 && this._elementToAttachTo.hasPointerCapture?.(this._mouseId)) {
  559. this._elementToAttachTo.releasePointerCapture(this._mouseId);
  560. }
  561. for (let inputIndex = PointerInput.LeftClick; inputIndex <= PointerInput.BrowserForward; inputIndex++) {
  562. if (pointer[inputIndex] === 1) {
  563. pointer[inputIndex] = 0;
  564. const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Mouse, 0, inputIndex, 0, this, this._elementToAttachTo);
  565. this._onInputChanged(DeviceType.Mouse, 0, deviceEvent);
  566. }
  567. }
  568. }
  569. // Handle Active Touches
  570. if (this.isDeviceAvailable(DeviceType.Touch)) {
  571. const pointer = this._inputs[DeviceType.Touch];
  572. for (let deviceSlot = 0; deviceSlot < this._activeTouchIds.length; deviceSlot++) {
  573. const pointerId = this._activeTouchIds[deviceSlot];
  574. if (this._elementToAttachTo.hasPointerCapture?.(pointerId)) {
  575. this._elementToAttachTo.releasePointerCapture(pointerId);
  576. }
  577. if (pointerId !== -1 && pointer[deviceSlot]?.[PointerInput.LeftClick] === 1) {
  578. pointer[deviceSlot][PointerInput.LeftClick] = 0;
  579. const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Touch, deviceSlot, PointerInput.LeftClick, 0, this, this._elementToAttachTo, pointerId);
  580. this._onInputChanged(DeviceType.Touch, deviceSlot, deviceEvent);
  581. this._activeTouchIds[deviceSlot] = -1;
  582. this._onDeviceDisconnected(DeviceType.Touch, deviceSlot);
  583. }
  584. }
  585. }
  586. };
  587. this._pointerWheelEvent = (evt) => {
  588. const deviceType = DeviceType.Mouse;
  589. const deviceSlot = 0;
  590. if (!this._inputs[deviceType]) {
  591. this._inputs[deviceType] = [];
  592. }
  593. if (!this._inputs[deviceType][deviceSlot]) {
  594. this._pointerActive = true;
  595. this._registerDevice(deviceType, deviceSlot, MAX_POINTER_INPUTS);
  596. }
  597. const pointer = this._inputs[deviceType][deviceSlot];
  598. if (pointer) {
  599. pointer[PointerInput.MouseWheelX] = evt.deltaX || 0;
  600. pointer[PointerInput.MouseWheelY] = evt.deltaY || evt.wheelDelta || 0;
  601. pointer[PointerInput.MouseWheelZ] = evt.deltaZ || 0;
  602. const deviceEvent = evt;
  603. // By default, there is no pointerId for mouse wheel events so we'll add one here
  604. // This logic was originally in the InputManager but was added here to make the
  605. // InputManager more platform-agnostic
  606. if (evt.pointerId === undefined) {
  607. evt.pointerId = this._mouseId;
  608. }
  609. if (pointer[PointerInput.MouseWheelX] !== 0) {
  610. deviceEvent.inputIndex = PointerInput.MouseWheelX;
  611. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  612. }
  613. if (pointer[PointerInput.MouseWheelY] !== 0) {
  614. deviceEvent.inputIndex = PointerInput.MouseWheelY;
  615. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  616. }
  617. if (pointer[PointerInput.MouseWheelZ] !== 0) {
  618. deviceEvent.inputIndex = PointerInput.MouseWheelZ;
  619. this._onInputChanged(deviceType, deviceSlot, deviceEvent);
  620. }
  621. }
  622. };
  623. // Workaround for MacOS Chromium Browsers for lost pointer capture bug
  624. if (this._usingMacOS && this._isUsingChromium) {
  625. this._pointerMacOSChromeOutEvent = (evt) => {
  626. if (evt.buttons > 1) {
  627. this._pointerCancelEvent(evt);
  628. }
  629. };
  630. this._elementToAttachTo.addEventListener("lostpointercapture", this._pointerMacOSChromeOutEvent);
  631. }
  632. this._elementToAttachTo.addEventListener(this._eventPrefix + "move", this._pointerMoveEvent);
  633. this._elementToAttachTo.addEventListener(this._eventPrefix + "down", this._pointerDownEvent);
  634. this._elementToAttachTo.addEventListener(this._eventPrefix + "up", this._pointerUpEvent);
  635. this._elementToAttachTo.addEventListener(this._eventPrefix + "cancel", this._pointerCancelEvent);
  636. this._elementToAttachTo.addEventListener("blur", this._pointerBlurEvent);
  637. this._elementToAttachTo.addEventListener(this._wheelEventName, this._pointerWheelEvent, passiveSupported ? { passive: false } : false);
  638. // Since there's no up or down event for mouse wheel or delta x/y, clear mouse values at end of frame
  639. this._pointerInputClearObserver = this._engine.onEndFrameObservable.add(() => {
  640. if (this.isDeviceAvailable(DeviceType.Mouse)) {
  641. const pointer = this._inputs[DeviceType.Mouse][0];
  642. pointer[PointerInput.MouseWheelX] = 0;
  643. pointer[PointerInput.MouseWheelY] = 0;
  644. pointer[PointerInput.MouseWheelZ] = 0;
  645. }
  646. });
  647. }
  648. /**
  649. * Handle all actions that come from gamepad interaction
  650. */
  651. _handleGamepadActions() {
  652. this._gamepadConnectedEvent = (evt) => {
  653. this._addGamePad(evt.gamepad);
  654. };
  655. this._gamepadDisconnectedEvent = (evt) => {
  656. if (this._gamepads) {
  657. const deviceType = this._getGamepadDeviceType(evt.gamepad.id);
  658. const deviceSlot = evt.gamepad.index;
  659. this._unregisterDevice(deviceType, deviceSlot);
  660. delete this._gamepads[deviceSlot];
  661. }
  662. };
  663. window.addEventListener("gamepadconnected", this._gamepadConnectedEvent);
  664. window.addEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
  665. }
  666. /**
  667. * Update all non-event based devices with each frame
  668. * @param deviceType Enum specifying device type
  669. * @param deviceSlot "Slot" or index that device is referenced in
  670. * @param inputIndex Id of input to be checked
  671. */
  672. _updateDevice(deviceType, deviceSlot, inputIndex) {
  673. // Gamepads
  674. const gp = navigator.getGamepads()[deviceSlot];
  675. if (gp && deviceType === this._gamepads[deviceSlot]) {
  676. const device = this._inputs[deviceType][deviceSlot];
  677. if (inputIndex >= gp.buttons.length) {
  678. device[inputIndex] = gp.axes[inputIndex - gp.buttons.length].valueOf();
  679. }
  680. else {
  681. device[inputIndex] = gp.buttons[inputIndex].value;
  682. }
  683. }
  684. }
  685. /**
  686. * Gets DeviceType from the device name
  687. * @param deviceName Name of Device from DeviceInputSystem
  688. * @returns DeviceType enum value
  689. */
  690. _getGamepadDeviceType(deviceName) {
  691. if (deviceName.indexOf("054c") !== -1) {
  692. // DualShock 4 Gamepad
  693. return deviceName.indexOf("0ce6") !== -1 ? DeviceType.DualSense : DeviceType.DualShock;
  694. }
  695. else if (deviceName.indexOf("Xbox One") !== -1 || deviceName.search("Xbox 360") !== -1 || deviceName.search("xinput") !== -1) {
  696. // Xbox Gamepad
  697. return DeviceType.Xbox;
  698. }
  699. else if (deviceName.indexOf("057e") !== -1) {
  700. // Switch Gamepad
  701. return DeviceType.Switch;
  702. }
  703. return DeviceType.Generic;
  704. }
  705. /**
  706. * Get DeviceType from a given pointer/mouse/touch event.
  707. * @param evt PointerEvent to evaluate
  708. * @returns DeviceType interpreted from event
  709. */
  710. _getPointerType(evt) {
  711. let deviceType = DeviceType.Mouse;
  712. if (evt.pointerType === "touch" || evt.pointerType === "pen" || evt.touches) {
  713. deviceType = DeviceType.Touch;
  714. }
  715. return deviceType;
  716. }
  717. }
  718. //# sourceMappingURL=webDeviceInputSystem.js.map