Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add touchenter event, add JSDoc, small refactor #4819

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"baseUrl": ".",
"checkJs": true,
"module": "es6",
"target": "es6"
"target": "es6",
"strictNullChecks": true
},
"include": ["src/**/*"]
}
158 changes: 125 additions & 33 deletions src/framework/input/element-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<number, {x: number, y: number, camera: import('../components/camera/component.js').CameraComponent, element: import('../components/element/component.js').ElementComponent}>}
*/
this._touchedElements = {};
this._touchesForWhichTouchLeaveHasFired = {};
this._selectedElements = {};
Expand Down Expand Up @@ -543,30 +559,38 @@ class ElementInput {
this._onElementMouseEvent('mousewheel', event);
}

/**
* @param {TouchEvent} event - The TouchEvent.
* @returns {Object<number, {x: number, y: number, camera: import('../components/camera/component.js').CameraComponent, element: import('../components/element/component.js').ElementComponent}>} - Object
* containing information about touched ElementComponent's.
* @private
*/
_determineTouchedElements(event) {
/** @type {Object<number, {x: number, y: number, camera: import('../components/camera/component.js').CameraComponent, element: import('../components/element/component.js').ElementComponent}>} */
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
for (let i = cameras.length - 1; i >= 0; i--) {
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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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];
Expand All @@ -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/
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should fire touchenter if a user has clicked outside the element and moved into it as well. Seems odd to only fire it if we started in an element, leave and then re-enter

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;

Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/platform/input/touch-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down