diff --git a/jsconfig.json b/jsconfig.json index d460f80cdbe..7b90c99f40c 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -3,7 +3,8 @@ "baseUrl": ".", "checkJs": true, "module": "es6", - "target": "es6" + "target": "es6", + "strictNullChecks": true }, "include": ["src/**/*"] } \ No newline at end of file diff --git a/src/framework/input/element-input.js b/src/framework/input/element-input.js index 072a47ffb86..3a1d701dc66 100644 --- a/src/framework/input/element-input.js +++ b/src/framework/input/element-input.js @@ -278,14 +278,14 @@ class ElementTouchEvent extends ElementInputEvent { * The Touch objects representing all current points of contact with the surface, * regardless of target or changed status. * - * @type {Touch[]} + * @type {TouchList} */ this.touches = event.touches; /** * The Touch objects representing individual points of contact whose states changed between * the previous touch event and this one. * - * @type {Touch[]} + * @type {TouchList} */ this.changedTouches = event.changedTouches; this.x = x; @@ -363,9 +363,25 @@ class ElementInput { this._touchmoveHandler = this._handleTouchMove.bind(this); this._sortHandler = this._sortElements.bind(this); + /** + * @type {import('../components/element/component.js').ElementComponent[]} + * @ignore + */ this._elements = []; + /** + * @type {import('../components/element/component.js').ElementComponent|null} + * @ignore + */ this._hoveredElement = null; + /** + * @type {import('../components/element/component.js').ElementComponent|null} + * @ignore + */ this._pressedElement = null; + /** + * @ignore + * @type {Object} + */ this._touchedElements = {}; this._touchesForWhichTouchLeaveHasFired = {}; this._selectedElements = {}; @@ -543,10 +559,17 @@ class ElementInput { this._onElementMouseEvent('mousewheel', event); } + /** + * @param {TouchEvent} event - The TouchEvent. + * @returns {Object} - Object + * containing information about touched ElementComponent's. + * @private + */ _determineTouchedElements(event) { + /** @type {Object} */ const touchedElements = {}; const cameras = this.app.systems.camera.cameras; - + const { changedTouches } = event; // check cameras from last to front // so that elements that are drawn above others // receive events first @@ -554,19 +577,20 @@ class ElementInput { const camera = cameras[i]; let done = 0; - const len = event.changedTouches.length; + const len = changedTouches.length; for (let j = 0; j < len; j++) { - if (touchedElements[event.changedTouches[j].identifier]) { + const changedTouch = changedTouches[j]; + if (touchedElements[changedTouch.identifier]) { done++; continue; } - const coords = this._calcTouchCoords(event.changedTouches[j]); + const coords = this._calcTouchCoords(changedTouch); const element = this._getTargetElementByCoords(camera, coords.x, coords.y); if (element) { done++; - touchedElements[event.changedTouches[j].identifier] = { + touchedElements[changedTouch.identifier] = { element: element, camera: camera, x: coords.x, @@ -594,14 +618,12 @@ class ElementInput { const oldTouchInfo = this._touchedElements[touch.identifier]; if (newTouchInfo && (!oldTouchInfo || newTouchInfo.element !== oldTouchInfo.element)) { - this._fireEvent(event.type, new ElementTouchEvent(event, newTouchInfo.element, newTouchInfo.camera, newTouchInfo.x, newTouchInfo.y, touch)); + this._fireEvent('touchstart', new ElementTouchEvent(event, newTouchInfo.element, newTouchInfo.camera, newTouchInfo.x, newTouchInfo.y, touch)); this._touchesForWhichTouchLeaveHasFired[touch.identifier] = false; } } - for (const touchId in newTouchedElements) { - this._touchedElements[touchId] = newTouchedElements[touchId]; - } + Object.assign(this._touchedElements, newTouchedElements); } _handleTouchEnd(event) { @@ -623,10 +645,7 @@ class ElementInput { if (!touchInfo) continue; - const element = touchInfo.element; - const camera = touchInfo.camera; - const x = touchInfo.x; - const y = touchInfo.y; + const { element, camera, x, y } = touchInfo; delete this._touchedElements[touch.identifier]; delete this._touchesForWhichTouchLeaveHasFired[touch.identifier]; @@ -651,6 +670,10 @@ class ElementInput { } } + /** + * @param {globalThis.TouchEvent} event - The TouchEvent. + * @private + */ _handleTouchMove(event) { // call preventDefault to avoid issues in Chrome Android: // http://wilsonpage.co.uk/touch-events-in-chrome-android/ @@ -659,33 +682,44 @@ class ElementInput { if (!this._enabled) return; const newTouchedElements = this._determineTouchedElements(event); - + const { _touchedElements, _touchesForWhichTouchLeaveHasFired } = this; for (let i = 0, len = event.changedTouches.length; i < len; i++) { const touch = event.changedTouches[i]; - const newTouchInfo = newTouchedElements[touch.identifier]; - const oldTouchInfo = this._touchedElements[touch.identifier]; - + const { identifier } = touch; + const newTouchInfo = newTouchedElements[identifier]; + const oldTouchInfo = _touchedElements[identifier]; if (oldTouchInfo) { - const coords = this._calcTouchCoords(touch); - + const { x, y } = this._calcTouchCoords(touch); + const leaveHasFired = _touchesForWhichTouchLeaveHasFired[identifier]; + const leftPreviousElement = !newTouchInfo || newTouchInfo.element !== oldTouchInfo.element; + const enterPreviousElement = newTouchInfo && newTouchInfo.element === oldTouchInfo.element; // Fire touchleave if we've left the previously touched element - if ((!newTouchInfo || newTouchInfo.element !== oldTouchInfo.element) && !this._touchesForWhichTouchLeaveHasFired[touch.identifier]) { - this._fireEvent('touchleave', new ElementTouchEvent(event, oldTouchInfo.element, oldTouchInfo.camera, coords.x, coords.y, touch)); - + if (leftPreviousElement && !leaveHasFired) { + this._fireEvent('touchleave', new ElementTouchEvent(event, oldTouchInfo.element, oldTouchInfo.camera, x, y, touch)); // Flag that touchleave has been fired for this touch, so that we don't // re-fire it on the next touchmove. This is required because touchmove // events keep on firing for the same element until the touch ends, even // if the touch position moves away from the element. Touchleave, on the // other hand, should fire once when the touch position moves away from // the element and then not re-fire again within the same touch session. - this._touchesForWhichTouchLeaveHasFired[touch.identifier] = true; + _touchesForWhichTouchLeaveHasFired[identifier] = true; } - - this._fireEvent('touchmove', new ElementTouchEvent(event, oldTouchInfo.element, oldTouchInfo.camera, coords.x, coords.y, touch)); + // And fire touchenter if we've entered the previously left touched element again + if (enterPreviousElement && leaveHasFired) { + this._fireEvent('touchenter', new ElementTouchEvent(event, oldTouchInfo.element, oldTouchInfo.camera, x, y, touch)); + _touchesForWhichTouchLeaveHasFired[identifier] = false; + } + // Always send touchmove, touched or not, e.g. scrolling behaviour depends on it + this._fireEvent('touchmove', new ElementTouchEvent(event, oldTouchInfo.element, oldTouchInfo.camera, x, y, touch)); } } } + /** + * @param {'mouseup'|'mousedown'|'mousemove'|'mousewheel'} eventType - The event type. + * @param {globalThis.MouseEvent} event - The MouseEvent. + * @private + */ _onElementMouseEvent(eventType, event) { let element = null; @@ -745,7 +779,6 @@ class ElementInput { const lastTouchUp = this._clickedEntities[guid] || 0; const dt = Date.now() - lastTouchUp; fireClick = dt > 300; - // We do not check another time, so the worst thing that can happen is one ignored click in 300ms. delete this._clickedEntities[guid]; } @@ -859,6 +892,11 @@ class ElementInput { } } + /** + * @param {'click'|'mouseup'|'mousedown'|'mousemove'|'mouseenter'|'mouseleave'|'mousewheel'|'touchleave'|'touchmove'|'selectleave'|'selectenter'|'selectstart'|'selectend'|'touchstart'|'touchenter'} name - The name. + * @param {ElementMouseEvent|ElementTouchEvent|ElementSelectEvent} evt - The Element{Mouse/Touch/Select}Event. + * @private + */ _fireEvent(name, evt) { let element = evt.element; while (true) { @@ -875,6 +913,10 @@ class ElementInput { } } + /** + * @param {globalThis.MouseEvent} event - The MouseEvent. + * @private + */ _calcMouseCoords(event) { const rect = this._target.getBoundingClientRect(); const left = Math.floor(rect.left); @@ -883,6 +925,11 @@ class ElementInput { targetY = (event.clientY - top); } + /** + * @param {globalThis.Touch} touch - The Touch. + * @returns {{x: number, y: number}} X and Y coordinate. + * @private + */ _calcTouchCoords(touch) { let totalOffsetX = 0; let totalOffsetY = 0; @@ -905,6 +952,12 @@ class ElementInput { }; } + /** + * @param {import('../components/element/component.js').ElementComponent} a - First element + * @param {import('../components/element/component.js').ElementComponent} b - Second Element + * @returns {number} Returns -1|0|1 to specify the relative position. + * @private + */ _sortElements(a, b) { const layerOrder = this.app.scene.layers.sortTransparentLayers(a.layers, b.layers); if (layerOrder !== 0) return layerOrder; @@ -923,6 +976,15 @@ class ElementInput { return b.drawOrder - a.drawOrder; } + /** + * Get the target element by coordinates. + * + * @param {import('../components/camera/component.js').CameraComponent} camera - The camera component. + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @returns {import('../components/element/component.js').ElementComponent|null} Returns ElementComponent or null. + * @private + */ _getTargetElementByCoords(camera, x, y) { // calculate screen-space and 3d-space rays const rayScreen = this._calculateRayScreen(x, y, camera, rayA) ? rayA : null; @@ -945,6 +1007,13 @@ class ElementInput { return this._getTargetElement(camera, rayScreen, ray3d); } + /** + * @param {import('../components/camera/component.js').CameraComponent} camera - The CameraComponent. + * @param {Ray} rayScreen - The rayScreen. + * @param {Ray} ray3d - The ray3d. + * @returns {import('../components/element/component.js').ElementComponent|null} Returns ElementComponent or null. + * @private + */ _getTargetElement(camera, rayScreen, ray3d) { let result = null; let closestDistance3d = Infinity; @@ -1030,6 +1099,14 @@ class ElementInput { return false; } + /** + * @param {number} x - The X coordinate. + * @param {number} y - The Y coordinate. + * @param {import('../components/camera/component.js').CameraComponent} camera - The CameraComponent. + * @param {Ray} ray - The ray. + * @returns {boolean} Whether window coords are within camera rect + * @private + */ _calculateRay3d(x, y, camera, ray) { const sw = this._target.clientWidth; const sh = this._target.clientHeight; @@ -1066,6 +1143,13 @@ class ElementInput { return false; } + /** + * @param {Ray} ray - The ray. + * @param {import('../components/element/component.js').ElementComponent} element - The ElementComponent. + * @param {boolean} screen - If true, calculate scale to screen, otherwise to world. + * @returns {number} -1 or distance squared. + * @private + */ _checkElement(ray, element, screen) { // ensure click is contained by any mask first if (element.maskedBy) { @@ -1086,11 +1170,19 @@ class ElementInput { return intersectLineQuad(ray.origin, ray.end, corners); } - // In most cases the corners used for hit testing will just be the element's - // screen corners. However, in cases where the element has additional hit - // padding specified, we need to expand the screenCorners to incorporate the - // padding. - // NOTE: Used by Editor for visualization in the viewport + /** + * In most cases the corners used for hit testing will just be the element's + * screen corners. However, in cases where the element has additional hit + * padding specified, we need to expand the screenCorners to incorporate the + * padding. + * NOTE: Used by Editor for visualization in the viewport + * + * @param {import('../components/element/component.js').ElementComponent} element - The ElementComponent. + * @param {Vec3[]} screenOrWorldCorners - Array of four corners. + * @param {Vec3} scale - The scale. + * @returns {Vec3[]} Array of four corners. + * @private + */ static buildHitCorners(element, screenOrWorldCorners, scale) { let hitCorners = screenOrWorldCorners; const button = element.entity && element.entity.button; diff --git a/src/platform/input/touch-event.js b/src/platform/input/touch-event.js index aafaa6b93da..e7740b69686 100644 --- a/src/platform/input/touch-event.js +++ b/src/platform/input/touch-event.js @@ -133,7 +133,7 @@ class TouchEvent { * * @param {number} id - The identifier of the touch. * @param {Touch[]|null} list - An array of touches to search. - * @returns {Touch} The {@link Touch} object or null. + * @returns {Touch|null} The {@link Touch} object or null. */ getTouchById(id, list) { for (let i = 0, l = list.length; i < l; i++) {