@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
758 lines • 39.5 kB
JavaScript
import { IsNavigatorAvailable } from "../Misc/domManagement.js";
import { Tools } from "../Misc/tools.js";
import { DeviceEventFactory } from "./eventFactory.js";
import { DeviceType, PointerInput } from "./InputDevices/deviceEnums.js";
// eslint-disable-next-line @typescript-eslint/naming-convention
const MAX_KEYCODES = 255;
// eslint-disable-next-line @typescript-eslint/naming-convention
const MAX_POINTER_INPUTS = Object.keys(PointerInput).length / 2;
/** @internal */
export class WebDeviceInputSystem {
/**
* Constructor for the WebDeviceInputSystem
* @param engine Engine to reference
* @param onDeviceConnected Callback to execute when device is connected
* @param onDeviceDisconnected Callback to execute when device is disconnected
* @param onInputChanged Callback to execute when input changes on device
*/
constructor(engine, onDeviceConnected, onDeviceDisconnected, onInputChanged) {
// Private Members
this._inputs = [];
this._keyboardActive = false;
this._pointerActive = false;
this._usingSafari = Tools.IsSafari();
// Found solution for determining if MacOS is being used here:
// https://stackoverflow.com/questions/10527983/best-way-to-detect-mac-os-x-or-windows-computers-with-javascript-or-jquery
this._usingMacOS = IsNavigatorAvailable() && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._keyboardDownEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._keyboardUpEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._keyboardBlurEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerMoveEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerDownEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerUpEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerCancelEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerCancelTouch = (pointerId) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerLeaveEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerWheelEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerBlurEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._pointerMacOSChromeOutEvent = (evt) => { };
this._eventsAttached = false;
this._mouseId = -1;
this._isUsingFirefox = IsNavigatorAvailable() && navigator.userAgent && navigator.userAgent.indexOf("Firefox") !== -1;
this._isUsingChromium = IsNavigatorAvailable() && navigator.userAgent && navigator.userAgent.indexOf("Chrome") !== -1;
this._maxTouchPoints = 0;
this._pointerInputClearObserver = null;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._gamepadConnectedEvent = (evt) => { };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._gamepadDisconnectedEvent = (evt) => { };
this._eventPrefix = Tools.GetPointerPrefix(engine);
this._engine = engine;
this._onDeviceConnected = onDeviceConnected;
this._onDeviceDisconnected = onDeviceDisconnected;
this._onInputChanged = onInputChanged;
// If we need a pointerId, set one for future use
this._mouseId = this._isUsingFirefox ? 0 : 1;
this._enableEvents();
if (this._usingMacOS) {
this._metaKeys = [];
}
// Set callback to enable event handler switching when inputElement changes
if (!this._engine._onEngineViewChanged) {
this._engine._onEngineViewChanged = () => {
this._enableEvents();
};
}
}
// Public functions
/**
* Checks for current device input value, given an id and input index. Throws exception if requested device not initialized.
* @param deviceType Enum specifying device type
* @param deviceSlot "Slot" or index that device is referenced in
* @param inputIndex Id of input to be checked
* @returns Current value of input
*/
pollInput(deviceType, deviceSlot, inputIndex) {
const device = this._inputs[deviceType][deviceSlot];
if (!device) {
// eslint-disable-next-line no-throw-literal
throw `Unable to find device ${DeviceType[deviceType]}`;
}
if (deviceType >= DeviceType.DualShock && deviceType <= DeviceType.DualSense) {
this._updateDevice(deviceType, deviceSlot, inputIndex);
}
const currentValue = device[inputIndex];
if (currentValue === undefined) {
// eslint-disable-next-line no-throw-literal
throw `Unable to find input ${inputIndex} for device ${DeviceType[deviceType]} in slot ${deviceSlot}`;
}
if (inputIndex === PointerInput.Move) {
Tools.Warn(`Unable to provide information for PointerInput.Move. Try using PointerInput.Horizontal or PointerInput.Vertical for move data.`);
}
return currentValue;
}
/**
* Check for a specific device in the DeviceInputSystem
* @param deviceType Type of device to check for
* @returns bool with status of device's existence
*/
isDeviceAvailable(deviceType) {
return this._inputs[deviceType] !== undefined;
}
/**
* Dispose of all the eventlisteners
*/
dispose() {
// Callbacks
this._onDeviceConnected = () => { };
this._onDeviceDisconnected = () => { };
this._onInputChanged = () => { };
delete this._engine._onEngineViewChanged;
if (this._elementToAttachTo) {
this._disableEvents();
}
}
/**
* Enable listening for user input events
*/
_enableEvents() {
const inputElement = this?._engine.getInputElement();
if (inputElement && (!this._eventsAttached || this._elementToAttachTo !== inputElement)) {
// Remove events before adding to avoid double events or simultaneous events on multiple canvases
this._disableEvents();
// If the inputs array has already been created, zero it out to before setting up events
if (this._inputs) {
for (const inputs of this._inputs) {
if (inputs) {
for (const deviceSlotKey in inputs) {
const deviceSlot = +deviceSlotKey;
const device = inputs[deviceSlot];
if (device) {
for (let inputIndex = 0; inputIndex < device.length; inputIndex++) {
device[inputIndex] = 0;
}
}
}
}
}
}
this._elementToAttachTo = inputElement;
// Set tab index for the inputElement to the engine's canvasTabIndex, if and only if the element's tab index is -1
this._elementToAttachTo.tabIndex = this._elementToAttachTo.tabIndex !== -1 ? this._elementToAttachTo.tabIndex : this._engine.canvasTabIndex;
this._handleKeyActions();
this._handlePointerActions();
this._handleGamepadActions();
this._eventsAttached = true;
// Check for devices that are already connected but aren't registered. Currently, only checks for gamepads and mouse
this._checkForConnectedDevices();
}
}
/**
* Disable listening for user input events
*/
_disableEvents() {
if (this._elementToAttachTo) {
// Blur Events
this._elementToAttachTo.removeEventListener("blur", this._keyboardBlurEvent);
this._elementToAttachTo.removeEventListener("blur", this._pointerBlurEvent);
// Keyboard Events
this._elementToAttachTo.removeEventListener("keydown", this._keyboardDownEvent);
this._elementToAttachTo.removeEventListener("keyup", this._keyboardUpEvent);
// Pointer Events
this._elementToAttachTo.removeEventListener(this._eventPrefix + "move", this._pointerMoveEvent);
this._elementToAttachTo.removeEventListener(this._eventPrefix + "down", this._pointerDownEvent);
this._elementToAttachTo.removeEventListener(this._eventPrefix + "up", this._pointerUpEvent);
this._elementToAttachTo.removeEventListener(this._eventPrefix + "cancel", this._pointerCancelEvent);
this._elementToAttachTo.removeEventListener(this._eventPrefix + "leave", this._pointerLeaveEvent);
this._elementToAttachTo.removeEventListener(this._wheelEventName, this._pointerWheelEvent);
if (this._usingMacOS && this._isUsingChromium) {
this._elementToAttachTo.removeEventListener("lostpointercapture", this._pointerMacOSChromeOutEvent);
}
// Gamepad Events
window.removeEventListener("gamepadconnected", this._gamepadConnectedEvent);
window.removeEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
}
if (this._pointerInputClearObserver) {
this._engine.onEndFrameObservable.remove(this._pointerInputClearObserver);
}
this._eventsAttached = false;
}
/**
* Checks for existing connections to devices and register them, if necessary
* Currently handles gamepads and mouse
*/
_checkForConnectedDevices() {
if (navigator.getGamepads) {
const gamepads = navigator.getGamepads();
for (const gamepad of gamepads) {
if (gamepad) {
this._addGamePad(gamepad);
}
}
}
// If the device in use has mouse capabilities, pre-register mouse
if (typeof matchMedia === "function" && matchMedia("(pointer:fine)").matches) {
// This will provide a dummy value for the cursor position and is expected to be overridden when the first mouse event happens.
// There isn't any good way to get the current position outside of a pointer event so that's why this was done.
this._addPointerDevice(DeviceType.Mouse, 0, 0, 0);
}
}
// Private functions
/**
* Add a gamepad to the DeviceInputSystem
* @param gamepad A single DOM Gamepad object
*/
_addGamePad(gamepad) {
const deviceType = this._getGamepadDeviceType(gamepad.id);
const deviceSlot = gamepad.index;
this._gamepads = this._gamepads || new Array(gamepad.index + 1);
this._registerDevice(deviceType, deviceSlot, gamepad.buttons.length + gamepad.axes.length);
this._gamepads[deviceSlot] = deviceType;
}
/**
* Add pointer device to DeviceInputSystem
* @param deviceType Type of Pointer to add
* @param deviceSlot Pointer ID (0 for mouse, pointerId for Touch)
* @param currentX Current X at point of adding
* @param currentY Current Y at point of adding
*/
_addPointerDevice(deviceType, deviceSlot, currentX, currentY) {
if (!this._pointerActive) {
this._pointerActive = true;
}
this._registerDevice(deviceType, deviceSlot, MAX_POINTER_INPUTS);
const pointer = this._inputs[deviceType][deviceSlot]; /* initialize our pointer position immediately after registration */
pointer[0] = currentX;
pointer[1] = currentY;
}
/**
* Add device and inputs to device array
* @param deviceType Enum specifying device type
* @param deviceSlot "Slot" or index that device is referenced in
* @param numberOfInputs Number of input entries to create for given device
*/
_registerDevice(deviceType, deviceSlot, numberOfInputs) {
if (deviceSlot === undefined) {
// eslint-disable-next-line no-throw-literal
throw `Unable to register device ${DeviceType[deviceType]} to undefined slot.`;
}
if (!this._inputs[deviceType]) {
this._inputs[deviceType] = {};
}
if (!this._inputs[deviceType][deviceSlot]) {
const device = new Array(numberOfInputs);
device.fill(0);
this._inputs[deviceType][deviceSlot] = device;
this._onDeviceConnected(deviceType, deviceSlot);
}
}
/**
* Given a specific device name, remove that device from the device map
* @param deviceType Enum specifying device type
* @param deviceSlot "Slot" or index that device is referenced in
*/
_unregisterDevice(deviceType, deviceSlot) {
if (this._inputs[deviceType][deviceSlot]) {
delete this._inputs[deviceType][deviceSlot];
this._onDeviceDisconnected(deviceType, deviceSlot);
}
}
/**
* Handle all actions that come from keyboard interaction
*/
_handleKeyActions() {
this._keyboardDownEvent = (evt) => {
if (!this._keyboardActive) {
this._keyboardActive = true;
this._registerDevice(DeviceType.Keyboard, 0, MAX_KEYCODES);
}
const kbKey = this._inputs[DeviceType.Keyboard][0];
if (kbKey) {
kbKey[evt.keyCode] = 1;
const deviceEvent = evt;
deviceEvent.inputIndex = evt.keyCode;
if (this._usingMacOS && evt.metaKey && evt.key !== "Meta") {
if (!this._metaKeys.includes(evt.keyCode)) {
this._metaKeys.push(evt.keyCode);
}
}
this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
}
};
this._keyboardUpEvent = (evt) => {
if (!this._keyboardActive) {
this._keyboardActive = true;
this._registerDevice(DeviceType.Keyboard, 0, MAX_KEYCODES);
}
const kbKey = this._inputs[DeviceType.Keyboard][0];
if (kbKey) {
kbKey[evt.keyCode] = 0;
const deviceEvent = evt;
deviceEvent.inputIndex = evt.keyCode;
if (this._usingMacOS && evt.key === "Meta" && this._metaKeys.length > 0) {
for (const keyCode of this._metaKeys) {
const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Keyboard, 0, keyCode, 0, this, this._elementToAttachTo);
kbKey[keyCode] = 0;
this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
}
this._metaKeys.splice(0, this._metaKeys.length);
}
this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
}
};
this._keyboardBlurEvent = () => {
if (this._keyboardActive) {
const kbKey = this._inputs[DeviceType.Keyboard][0];
for (let i = 0; i < kbKey.length; i++) {
if (kbKey[i] !== 0) {
kbKey[i] = 0;
const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Keyboard, 0, i, 0, this, this._elementToAttachTo);
this._onInputChanged(DeviceType.Keyboard, 0, deviceEvent);
}
}
if (this._usingMacOS) {
this._metaKeys.splice(0, this._metaKeys.length);
}
}
};
this._elementToAttachTo.addEventListener("keydown", this._keyboardDownEvent);
this._elementToAttachTo.addEventListener("keyup", this._keyboardUpEvent);
this._elementToAttachTo.addEventListener("blur", this._keyboardBlurEvent);
}
/**
* Handle all actions that come from pointer interaction
*/
_handlePointerActions() {
// If maxTouchPoints is defined, use that value. Otherwise, allow for a minimum for supported gestures like pinch
this._maxTouchPoints = (IsNavigatorAvailable() && navigator.maxTouchPoints) || 2;
if (!this._activeTouchIds) {
this._activeTouchIds = new Array(this._maxTouchPoints);
}
for (let i = 0; i < this._maxTouchPoints; i++) {
this._activeTouchIds[i] = -1;
}
this._pointerMoveEvent = (evt) => {
const deviceType = this._getPointerType(evt);
let deviceSlot = deviceType === DeviceType.Mouse ? 0 : this._activeTouchIds.indexOf(evt.pointerId);
// In the event that we're getting pointermove events from touch inputs that we aren't tracking,
// look for an available slot and retroactively connect it.
if (deviceType === DeviceType.Touch && deviceSlot === -1) {
const idx = this._activeTouchIds.indexOf(-1);
if (idx >= 0) {
deviceSlot = idx;
this._activeTouchIds[idx] = evt.pointerId;
// Because this is a "new" input, inform the connected callback
this._onDeviceConnected(deviceType, deviceSlot);
}
else {
// We can't find an open slot to store new pointer so just return (can only support max number of touches)
Tools.Warn(`Max number of touches exceeded. Ignoring touches in excess of ${this._maxTouchPoints}`);
return;
}
}
if (!this._inputs[deviceType]) {
this._inputs[deviceType] = {};
}
if (!this._inputs[deviceType][deviceSlot]) {
this._addPointerDevice(deviceType, deviceSlot, evt.clientX, evt.clientY);
}
const pointer = this._inputs[deviceType][deviceSlot];
if (pointer) {
const deviceEvent = evt;
deviceEvent.inputIndex = PointerInput.Move;
pointer[PointerInput.Horizontal] = evt.clientX;
pointer[PointerInput.Vertical] = evt.clientY;
// For touches that aren't started with a down, we need to set the button state to 1
if (deviceType === DeviceType.Touch && pointer[PointerInput.LeftClick] === 0) {
pointer[PointerInput.LeftClick] = 1;
}
if (evt.pointerId === undefined) {
evt.pointerId = this._mouseId;
}
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
// Lets Propagate the event for move with same position.
if (!this._usingSafari && evt.button !== -1) {
deviceEvent.inputIndex = evt.button + 2;
pointer[evt.button + 2] = pointer[evt.button + 2] ? 0 : 1; // Reverse state of button if evt.button has value
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
}
}
};
this._pointerDownEvent = (evt) => {
const deviceType = this._getPointerType(evt);
let deviceSlot = deviceType === DeviceType.Mouse ? 0 : evt.pointerId;
if (deviceType === DeviceType.Touch) {
// See if this pointerId is already using an existing slot
// (possible on some devices which raise the pointerMove event before the pointerDown event, e.g. when using a pen)
let idx = this._activeTouchIds.indexOf(evt.pointerId);
if (idx === -1) {
// If the pointerId wasn't already using a slot, find an open one
idx = this._activeTouchIds.indexOf(-1);
}
if (idx >= 0) {
deviceSlot = idx;
this._activeTouchIds[idx] = evt.pointerId;
}
else {
// We can't find an open slot to store new pointer so just return (can only support max number of touches)
Tools.Warn(`Max number of touches exceeded. Ignoring touches in excess of ${this._maxTouchPoints}`);
return;
}
}
if (!this._inputs[deviceType]) {
this._inputs[deviceType] = {};
}
if (!this._inputs[deviceType][deviceSlot]) {
this._addPointerDevice(deviceType, deviceSlot, evt.clientX, evt.clientY);
}
else if (deviceType === DeviceType.Touch) {
this._onDeviceConnected(deviceType, deviceSlot);
}
const pointer = this._inputs[deviceType][deviceSlot];
if (pointer) {
const previousHorizontal = pointer[PointerInput.Horizontal];
const previousVertical = pointer[PointerInput.Vertical];
if (deviceType === DeviceType.Mouse) {
// Mouse; Set pointerId if undefined
if (evt.pointerId === undefined) {
evt.pointerId = this._mouseId;
}
if (!document.pointerLockElement) {
try {
this._elementToAttachTo.setPointerCapture(this._mouseId);
}
catch (e) {
// DO NOTHING
}
}
}
else {
// Touch; Since touches are dynamically assigned, only set capture if we have an id
if (evt.pointerId && !document.pointerLockElement) {
try {
this._elementToAttachTo.setPointerCapture(evt.pointerId);
}
catch (e) {
// DO NOTHING
}
}
}
pointer[PointerInput.Horizontal] = evt.clientX;
pointer[PointerInput.Vertical] = evt.clientY;
pointer[evt.button + 2] = 1;
const deviceEvent = evt;
// NOTE: The +2 used here to is because PointerInput has the same value progression for its mouse buttons as PointerEvent.button
// However, we have our X and Y values front-loaded to group together the touch inputs but not break this progression
// EG. ([X, Y, Left-click], Middle-click, etc...)
deviceEvent.inputIndex = evt.button + 2;
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
if (previousHorizontal !== evt.clientX || previousVertical !== evt.clientY) {
deviceEvent.inputIndex = PointerInput.Move;
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
}
}
};
this._pointerUpEvent = (evt) => {
const deviceType = this._getPointerType(evt);
const deviceSlot = deviceType === DeviceType.Mouse ? 0 : this._activeTouchIds.indexOf(evt.pointerId);
if (deviceType === DeviceType.Touch) {
// If we're getting a pointerup event for a touch that isn't active, just return.
if (deviceSlot === -1) {
return;
}
else {
this._activeTouchIds[deviceSlot] = -1;
}
}
const pointer = this._inputs[deviceType]?.[deviceSlot];
let button = evt.button;
let shouldProcessPointerUp = pointer && pointer[button + 2] !== 0;
// Workaround for an issue in Firefox on MacOS only where the browser allows the user to change left button
// actions into right button actions by holding down control. If the user starts a drag with the control button
// down, then lifts control, then releases the mouse, we'll get mismatched up and down events (the down will be
// the right button, and the up will be the left button). In that specific case, where we get an up from a button
// which didn't have a corresponding down, and we are in Firefox on MacOS, we should process the up event as if it
// was from the other button.
// Ideally this would be fixed in Firefox so that if you start a drag with the control button down, then the button
// passed along to both pointer down and up would be the right button regardless of the order in which control and the
// mouse button were released.
// If Firefox makes a fix to ensure this is the case, this workaround can be removed.
// Relevant forum thread: https://forum.babylonjs.com/t/camera-pan-getting-stuck-in-firefox/57158
if (!shouldProcessPointerUp && this._isUsingFirefox && this._usingMacOS && pointer) {
// Try the other button (left or right button)
button = button === 2 ? 0 : 2;
shouldProcessPointerUp = pointer[button + 2] !== 0;
}
if (shouldProcessPointerUp) {
const previousHorizontal = pointer[PointerInput.Horizontal];
const previousVertical = pointer[PointerInput.Vertical];
pointer[PointerInput.Horizontal] = evt.clientX;
pointer[PointerInput.Vertical] = evt.clientY;
pointer[button + 2] = 0;
const deviceEvent = evt;
if (evt.pointerId === undefined) {
evt.pointerId = this._mouseId;
}
if (previousHorizontal !== evt.clientX || previousVertical !== evt.clientY) {
deviceEvent.inputIndex = PointerInput.Move;
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
}
// NOTE: The +2 used here to is because PointerInput has the same value progression for its mouse buttons as PointerEvent.button
// However, we have our X and Y values front-loaded to group together the touch inputs but not break this progression
// EG. ([X, Y, Left-click], Middle-click, etc...)
deviceEvent.inputIndex = button + 2;
if (deviceType === DeviceType.Mouse && this._mouseId >= 0 && this._elementToAttachTo.hasPointerCapture?.(this._mouseId)) {
this._elementToAttachTo.releasePointerCapture(this._mouseId);
}
else if (evt.pointerId && this._elementToAttachTo.hasPointerCapture?.(evt.pointerId)) {
this._elementToAttachTo.releasePointerCapture(evt.pointerId);
}
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
if (deviceType === DeviceType.Touch) {
this._onDeviceDisconnected(deviceType, deviceSlot);
}
}
};
this._pointerCancelTouch = (pointerId) => {
const deviceSlot = this._activeTouchIds.indexOf(pointerId);
// If we're getting a pointercancel event for a touch that isn't active, just return
if (deviceSlot === -1) {
return;
}
if (this._elementToAttachTo.hasPointerCapture?.(pointerId)) {
this._elementToAttachTo.releasePointerCapture(pointerId);
}
this._inputs[DeviceType.Touch][deviceSlot][PointerInput.LeftClick] = 0;
const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Touch, deviceSlot, PointerInput.LeftClick, 0, this, this._elementToAttachTo, pointerId);
this._onInputChanged(DeviceType.Touch, deviceSlot, deviceEvent);
this._activeTouchIds[deviceSlot] = -1;
this._onDeviceDisconnected(DeviceType.Touch, deviceSlot);
};
this._pointerCancelEvent = (evt) => {
if (evt.pointerType === "mouse") {
const pointer = this._inputs[DeviceType.Mouse][0];
if (this._mouseId >= 0 && this._elementToAttachTo.hasPointerCapture?.(this._mouseId)) {
this._elementToAttachTo.releasePointerCapture(this._mouseId);
}
for (let inputIndex = PointerInput.LeftClick; inputIndex <= PointerInput.BrowserForward; inputIndex++) {
if (pointer[inputIndex] === 1) {
pointer[inputIndex] = 0;
const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Mouse, 0, inputIndex, 0, this, this._elementToAttachTo);
this._onInputChanged(DeviceType.Mouse, 0, deviceEvent);
}
}
}
else {
this._pointerCancelTouch(evt.pointerId);
}
};
this._pointerLeaveEvent = (evt) => {
if (evt.pointerType === "pen") {
// If a pen leaves the hover range detectible by the hardware this event is raised and we need to cancel the operation
// Note that pen operations are treated as touch operations
this._pointerCancelTouch(evt.pointerId);
}
};
// Set Wheel Event Name, code originally from scene.inputManager
this._wheelEventName =
"onwheel" in document.createElement("div")
? "wheel" // Modern browsers support "wheel"
: document.onmousewheel !== undefined
? "mousewheel" // Webkit and IE support at least "mousewheel"
: "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox
// Code originally in scene.inputManager.ts
// Chrome reports warning in console if wheel listener doesn't set an explicit passive option.
// IE11 only supports captureEvent:boolean, not options:object, and it defaults to false.
// Feature detection technique copied from: https://github.com/github/eventlistener-polyfill (MIT license)
let passiveSupported = false;
const noop = function () { };
try {
const options = Object.defineProperty({}, "passive", {
get: function () {
passiveSupported = true;
},
});
this._elementToAttachTo.addEventListener("test", noop, options);
this._elementToAttachTo.removeEventListener("test", noop, options);
}
catch (e) {
/* */
}
this._pointerBlurEvent = () => {
// Handle mouse buttons
if (this.isDeviceAvailable(DeviceType.Mouse)) {
const pointer = this._inputs[DeviceType.Mouse][0];
if (this._mouseId >= 0 && this._elementToAttachTo.hasPointerCapture?.(this._mouseId)) {
this._elementToAttachTo.releasePointerCapture(this._mouseId);
}
for (let inputIndex = PointerInput.LeftClick; inputIndex <= PointerInput.BrowserForward; inputIndex++) {
if (pointer[inputIndex] === 1) {
pointer[inputIndex] = 0;
const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Mouse, 0, inputIndex, 0, this, this._elementToAttachTo);
this._onInputChanged(DeviceType.Mouse, 0, deviceEvent);
}
}
}
// Handle Active Touches
if (this.isDeviceAvailable(DeviceType.Touch)) {
const pointer = this._inputs[DeviceType.Touch];
for (let deviceSlot = 0; deviceSlot < this._activeTouchIds.length; deviceSlot++) {
const pointerId = this._activeTouchIds[deviceSlot];
if (this._elementToAttachTo.hasPointerCapture?.(pointerId)) {
this._elementToAttachTo.releasePointerCapture(pointerId);
}
if (pointerId !== -1 && pointer[deviceSlot]?.[PointerInput.LeftClick] === 1) {
pointer[deviceSlot][PointerInput.LeftClick] = 0;
const deviceEvent = DeviceEventFactory.CreateDeviceEvent(DeviceType.Touch, deviceSlot, PointerInput.LeftClick, 0, this, this._elementToAttachTo, pointerId);
this._onInputChanged(DeviceType.Touch, deviceSlot, deviceEvent);
this._activeTouchIds[deviceSlot] = -1;
this._onDeviceDisconnected(DeviceType.Touch, deviceSlot);
}
}
}
};
this._pointerWheelEvent = (evt) => {
const deviceType = DeviceType.Mouse;
const deviceSlot = 0;
if (!this._inputs[deviceType]) {
this._inputs[deviceType] = [];
}
if (!this._inputs[deviceType][deviceSlot]) {
this._pointerActive = true;
this._registerDevice(deviceType, deviceSlot, MAX_POINTER_INPUTS);
}
const pointer = this._inputs[deviceType][deviceSlot];
if (pointer) {
pointer[PointerInput.MouseWheelX] = evt.deltaX || 0;
pointer[PointerInput.MouseWheelY] = evt.deltaY || evt.wheelDelta || 0;
pointer[PointerInput.MouseWheelZ] = evt.deltaZ || 0;
const deviceEvent = evt;
// By default, there is no pointerId for mouse wheel events so we'll add one here
// This logic was originally in the InputManager but was added here to make the
// InputManager more platform-agnostic
if (evt.pointerId === undefined) {
evt.pointerId = this._mouseId;
}
if (pointer[PointerInput.MouseWheelX] !== 0) {
deviceEvent.inputIndex = PointerInput.MouseWheelX;
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
}
if (pointer[PointerInput.MouseWheelY] !== 0) {
deviceEvent.inputIndex = PointerInput.MouseWheelY;
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
}
if (pointer[PointerInput.MouseWheelZ] !== 0) {
deviceEvent.inputIndex = PointerInput.MouseWheelZ;
this._onInputChanged(deviceType, deviceSlot, deviceEvent);
}
}
};
// Workaround for MacOS Chromium Browsers for lost pointer capture bug
if (this._usingMacOS && this._isUsingChromium) {
this._pointerMacOSChromeOutEvent = (evt) => {
if (evt.buttons > 1) {
this._pointerCancelEvent(evt);
}
};
this._elementToAttachTo.addEventListener("lostpointercapture", this._pointerMacOSChromeOutEvent);
}
this._elementToAttachTo.addEventListener(this._eventPrefix + "move", this._pointerMoveEvent);
this._elementToAttachTo.addEventListener(this._eventPrefix + "down", this._pointerDownEvent);
this._elementToAttachTo.addEventListener(this._eventPrefix + "up", this._pointerUpEvent);
this._elementToAttachTo.addEventListener(this._eventPrefix + "cancel", this._pointerCancelEvent);
this._elementToAttachTo.addEventListener(this._eventPrefix + "leave", this._pointerLeaveEvent);
this._elementToAttachTo.addEventListener("blur", this._pointerBlurEvent);
this._elementToAttachTo.addEventListener(this._wheelEventName, this._pointerWheelEvent, passiveSupported ? { passive: false } : false);
// Since there's no up or down event for mouse wheel or delta x/y, clear mouse values at end of frame
this._pointerInputClearObserver = this._engine.onEndFrameObservable.add(() => {
if (this.isDeviceAvailable(DeviceType.Mouse)) {
const pointer = this._inputs[DeviceType.Mouse][0];
pointer[PointerInput.MouseWheelX] = 0;
pointer[PointerInput.MouseWheelY] = 0;
pointer[PointerInput.MouseWheelZ] = 0;
}
});
}
/**
* Handle all actions that come from gamepad interaction
*/
_handleGamepadActions() {
this._gamepadConnectedEvent = (evt) => {
this._addGamePad(evt.gamepad);
};
this._gamepadDisconnectedEvent = (evt) => {
if (this._gamepads) {
const deviceType = this._getGamepadDeviceType(evt.gamepad.id);
const deviceSlot = evt.gamepad.index;
this._unregisterDevice(deviceType, deviceSlot);
delete this._gamepads[deviceSlot];
}
};
window.addEventListener("gamepadconnected", this._gamepadConnectedEvent);
window.addEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
}
/**
* Update all non-event based devices with each frame
* @param deviceType Enum specifying device type
* @param deviceSlot "Slot" or index that device is referenced in
* @param inputIndex Id of input to be checked
*/
_updateDevice(deviceType, deviceSlot, inputIndex) {
// Gamepads
const gp = navigator.getGamepads()[deviceSlot];
if (gp && deviceType === this._gamepads[deviceSlot]) {
const device = this._inputs[deviceType][deviceSlot];
if (inputIndex >= gp.buttons.length) {
device[inputIndex] = gp.axes[inputIndex - gp.buttons.length].valueOf();
}
else {
device[inputIndex] = gp.buttons[inputIndex].value;
}
}
}
/**
* Gets DeviceType from the device name
* @param deviceName Name of Device from DeviceInputSystem
* @returns DeviceType enum value
*/
_getGamepadDeviceType(deviceName) {
if (deviceName.indexOf("054c") !== -1) {
// DualShock 4 Gamepad
return deviceName.indexOf("0ce6") !== -1 ? DeviceType.DualSense : DeviceType.DualShock;
}
else if (deviceName.indexOf("Xbox One") !== -1 || deviceName.search("Xbox 360") !== -1 || deviceName.search("xinput") !== -1) {
// Xbox Gamepad
return DeviceType.Xbox;
}
else if (deviceName.indexOf("057e") !== -1) {
// Switch Gamepad
return DeviceType.Switch;
}
return DeviceType.Generic;
}
/**
* Get DeviceType from a given pointer/mouse/touch event.
* @param evt PointerEvent to evaluate
* @returns DeviceType interpreted from event
*/
_getPointerType(evt) {
let deviceType = DeviceType.Mouse;
if (evt.pointerType === "touch" || evt.pointerType === "pen" || evt.touches) {
deviceType = DeviceType.Touch;
}
return deviceType;
}
}
//# sourceMappingURL=webDeviceInputSystem.js.map