diff --git a/packages/core/src/Engine.ts b/packages/core/src/Engine.ts index d55a7798ee..550c65afcb 100644 --- a/packages/core/src/Engine.ts +++ b/packages/core/src/Engine.ts @@ -703,6 +703,7 @@ export class Engine extends EventDispatcher { this._renderElementPool.garbageCollection(); this._textRenderElementPool.garbageCollection(); this._renderContext.garbageCollection(); + this.inputManager._gc(); const scenes = this._sceneManager._scenes.getLoopArray(); for (let i = 0, n = scenes.length; i < n; i++) { scenes[i]?.physics?._gc(); diff --git a/packages/core/src/input/InputManager.ts b/packages/core/src/input/InputManager.ts index 617fc4c366..e82d056fb7 100644 --- a/packages/core/src/input/InputManager.ts +++ b/packages/core/src/input/InputManager.ts @@ -181,6 +181,13 @@ export class InputManager { this._initialized && this._pointerManager._firePointerScript(scenes); } + /** + * @internal + */ + _gc(): void { + this._initialized && this._pointerManager._gc(); + } + /** * @internal */ diff --git a/packages/core/src/input/pointer/Pointer.ts b/packages/core/src/input/pointer/Pointer.ts index a7412bc2ed..298db706a3 100644 --- a/packages/core/src/input/pointer/Pointer.ts +++ b/packages/core/src/input/pointer/Pointer.ts @@ -1,9 +1,7 @@ import { Vector2 } from "@galacean/engine-math"; -import { ClearableObjectPool } from "../../utils/ClearableObjectPool"; import { DisorderedArray } from "../../utils/DisorderedArray"; import { PointerButton } from "../enums/PointerButton"; import { PointerPhase } from "../enums/PointerPhase"; -import { PointerEventData } from "./PointerEventData"; import { PointerEventEmitter } from "./emitter/PointerEventEmitter"; /** @@ -23,6 +21,8 @@ export class Pointer { pressedButtons: PointerButton; /** The position of the pointer in screen space pixel coordinates. */ position: Vector2 = new Vector2(); + /** The position of the pointer when it was last pressed down (in screen space pixel coordinates). */ + pressedPosition: Vector2 = new Vector2(); /** The change of the pointer. */ deltaPosition: Vector2 = new Vector2(); /** @internal */ @@ -49,16 +49,6 @@ export class Pointer { this.id = id; } - /** - * @internal - */ - _addEmitters) => PointerEventEmitter>( - type: T, - pool: ClearableObjectPool - ) { - this._emitters.push(new type(pool)); - } - /** * @internal */ diff --git a/packages/core/src/input/pointer/PointerEventData.ts b/packages/core/src/input/pointer/PointerEventData.ts index 89f9ae09b6..33dc41b001 100644 --- a/packages/core/src/input/pointer/PointerEventData.ts +++ b/packages/core/src/input/pointer/PointerEventData.ts @@ -1,4 +1,5 @@ import { Vector3 } from "@galacean/engine-math"; +import { Entity } from "../../Entity"; import { IPoolElement } from "../../utils/ObjectPool"; import { Pointer } from "./Pointer"; @@ -10,11 +11,17 @@ export class PointerEventData implements IPoolElement { pointer: Pointer; /** The position of the event trigger (in world space). */ worldPosition: Vector3 = new Vector3(); + /** The hit-tested target entity (deepest entity on the bubble path). */ + target: Entity = null; + /** The entity currently handling the event (same as target on non-bubbling fire, varies on bubble). */ + currentTarget: Entity = null; /** * @internal */ dispose() { this.pointer = null; + this.target = null; + this.currentTarget = null; } } diff --git a/packages/core/src/input/pointer/PointerManager.ts b/packages/core/src/input/pointer/PointerManager.ts index fc6d8a2010..71ad2178cf 100644 --- a/packages/core/src/input/pointer/PointerManager.ts +++ b/packages/core/src/input/pointer/PointerManager.ts @@ -116,10 +116,7 @@ export class PointerManager implements IInput { pointer = pointerPool[j]; if (!pointer) { pointer = pointerPool[j] = new Pointer(j); - engine._physicsInitialized && pointer._addEmitters(PhysicsPointerEventEmitter, eventPool); - PointerManager._pointerEventEmitters.forEach((emitter) => { - pointer._addEmitters(emitter, eventPool); - }); + this._addEmitters(pointer); } pointer._uniqueID = pointerId; pointer._events.push(evt); @@ -193,6 +190,13 @@ export class PointerManager implements IInput { } } + /** + * @internal + */ + _gc(): void { + this._eventPool.garbageCollection(); + } + /** * @internal */ @@ -201,6 +205,16 @@ export class PointerManager implements IInput { this._pointerPool.length = 0; } + private _addEmitters(pointer: Pointer): void { + const { _eventPool: eventPool, _engine: engine } = this; + const emitters = pointer._emitters; + engine._physicsInitialized && emitters.push(new PhysicsPointerEventEmitter(eventPool)); + const emitterTypes = PointerManager._pointerEventEmitters; + for (let i = 0, n = emitterTypes.length; i < n; i++) { + emitters.push(new emitterTypes[i](eventPool)); + } + } + private _onPointerEvent(evt: PointerEvent) { this._nativeEvents.push(evt); } @@ -245,6 +259,10 @@ export class PointerManager implements IInput { pointer._downMap[button] = frameCount; pointer._frameEvents |= PointerEventType.Down; pointer.phase = PointerPhase.Down; + pointer.pressedPosition.set( + (event.clientX - left) * widthPixelRatio, + (event.clientY - top) * heightPixelRatio + ); break; } case "pointerup": { diff --git a/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts b/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts index 5778f924a9..7ac7747a6b 100644 --- a/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts +++ b/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts @@ -53,13 +53,13 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { override processDrag(pointer: Pointer): void { const entity = this._draggedEntity; - entity && this._fireDrag(entity, this._createEventData(pointer)); + entity && this._fireDrag(entity, this._createEventData(pointer, entity)); } override processDown(pointer: Pointer): void { const entity = (this._pressedEntity = this._draggedEntity = this._enteredEntity); if (entity) { - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, entity); this._fireDown(entity, eventData); this._fireBeginDrag(entity, eventData); } @@ -69,14 +69,14 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { const { _enteredEntity: enteredEntity, _draggedEntity: draggedEntity } = this; if (enteredEntity) { const sameTarget = this._pressedEntity === enteredEntity; - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, enteredEntity); this._fireUp(enteredEntity, eventData); sameTarget && this._fireClick(enteredEntity, eventData); this._fireDrop(enteredEntity, eventData); } this._pressedEntity = null; if (draggedEntity) { - this._fireEndDrag(draggedEntity, this._createEventData(pointer)); + this._fireEndDrag(draggedEntity, this._createEventData(pointer, draggedEntity)); this._draggedEntity = null; } } @@ -84,13 +84,13 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { override processLeave(pointer: Pointer): void { const enteredEntity = this._enteredEntity; if (enteredEntity) { - this._fireExit(enteredEntity, this._createEventData(pointer)); + this._fireExit(enteredEntity, this._createEventData(pointer, enteredEntity)); this._enteredEntity = null; } const draggedEntity = this._draggedEntity; if (draggedEntity) { - this._fireEndDrag(draggedEntity, this._createEventData(pointer)); + this._fireEndDrag(draggedEntity, this._createEventData(pointer, draggedEntity)); this._draggedEntity = null; } this._pressedEntity = null; @@ -108,10 +108,10 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { const enteredEntity = this._enteredEntity; if (entity !== enteredEntity) { if (enteredEntity) { - this._fireExit(enteredEntity, this._createEventData(pointer)); + this._fireExit(enteredEntity, this._createEventData(pointer, enteredEntity)); } if (entity) { - this._fireEnter(entity, this._createEventData(pointer)); + this._fireEnter(entity, this._createEventData(pointer, entity)); } this._enteredEntity = entity; } diff --git a/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts b/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts index 6f6c62cdfb..98188891e6 100644 --- a/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts +++ b/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts @@ -32,10 +32,12 @@ export abstract class PointerEventEmitter { protected abstract _init(): void; - protected _createEventData(pointer: Pointer): PointerEventData { + protected _createEventData(pointer: Pointer, target: Entity, currentTarget: Entity = target): PointerEventData { const data = this._pool.get(); data.pointer = pointer; data.worldPosition.copyFrom(this._hitResult.point); + data.target = target; + data.currentTarget = currentTarget; return data; } diff --git a/packages/ui/src/input/UIPointerEventEmitter.ts b/packages/ui/src/input/UIPointerEventEmitter.ts index 83bf8f1484..5ea25e23c3 100644 --- a/packages/ui/src/input/UIPointerEventEmitter.ts +++ b/packages/ui/src/input/UIPointerEventEmitter.ts @@ -91,11 +91,11 @@ export class UIPointerEventEmitter extends PointerEventEmitter { } } if (camera.clearFlags & CameraClearFlags.Color) { - this._updateRaycast(null); + this._updateRaycast(null, pointer); return; } } - this._updateRaycast(null); + this._updateRaycast(null, pointer); } } @@ -128,10 +128,7 @@ export class UIPointerEventEmitter extends PointerEventEmitter { if (pressedPath.length > 0) { const common = UIPointerEventEmitter._tempArray0; if (this._findCommonInPath(enteredPath, pressedPath, common)) { - const eventData = this._createEventData(pointer); - for (let i = 0, n = common.length; i < n; i++) { - this._fireClick(common[i], eventData); - } + this._bubble(common, pointer, this._fireClick); common.length = 0; } } @@ -170,18 +167,17 @@ export class UIPointerEventEmitter extends PointerEventEmitter { this._enteredPath.length = this._pressedPath.length = this._draggedPath.length = 0; } - private _updateRaycast(element: UIRenderer, pointer: Pointer = null): void { + private _updateRaycast(element: UIRenderer | null, pointer: Pointer): void { const enteredPath = this._enteredPath; const curPath = this._composedPath(element, UIPointerEventEmitter._path); const add = UIPointerEventEmitter._tempArray0; const del = UIPointerEventEmitter._tempArray1; if (this._findDiffInPath(enteredPath, curPath, add, del)) { - const eventData = this._createEventData(pointer); for (let i = 0, n = add.length; i < n; i++) { - this._fireEnter(add[i], eventData); + this._fireEnter(add[i], this._createEventData(pointer, add[i])); } for (let i = 0, n = del.length; i < n; i++) { - this._fireExit(del[i], eventData); + this._fireExit(del[i], this._createEventData(pointer, del[i])); } const length = (enteredPath.length = curPath.length); @@ -193,7 +189,7 @@ export class UIPointerEventEmitter extends PointerEventEmitter { curPath.length = 0; } - private _composedPath(element: UIRenderer, path: Entity[]): Entity[] { + private _composedPath(element: UIRenderer | null, path: Entity[]): Entity[] { if (!element) { path.length = 0; return path; @@ -256,8 +252,9 @@ export class UIPointerEventEmitter extends PointerEventEmitter { private _bubble(path: Entity[], pointer: Pointer, fireEvent: FireEvent): void { const length = path.length; if (length <= 0) return; - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, path[0]); for (let i = 0; i < length; i++) { + eventData.currentTarget = path[i]; fireEvent(path[i], eventData); } } diff --git a/tests/src/core/input/InputManager.test.ts b/tests/src/core/input/InputManager.test.ts index f883f8fd42..0fb0efbb53 100644 --- a/tests/src/core/input/InputManager.test.ts +++ b/tests/src/core/input/InputManager.test.ts @@ -171,6 +171,41 @@ describe("InputManager", async () => { engine.update(); }); + it("pointer pressedPosition", () => { + // @ts-ignore + const { _pointerManager: pointerManager } = inputManager; + const { _target: target } = pointerManager; + const { left, top } = target.getBoundingClientRect(); + + // Frame 1: a pointerdown and a move to a different position are batched into one frame. + target.dispatchEvent(generatePointerEvent("pointerdown", 5, left + 1, top + 1)); + target.dispatchEvent(generatePointerEvent("pointermove", 5, left + 3, top + 3)); + engine.update(); + const pointer = inputManager.pointers[0]; + // @ts-ignore + const uniqueID = pointer._uniqueID; + const position1 = pointer.position.clone(); + const pressedPosition1 = pointer.pressedPosition.clone(); + + // Frame 2: keep dragging to yet another position. + target.dispatchEvent(generatePointerEvent("pointermove", 5, left + 6, top + 6)); + engine.update(); + const position2 = pointer.position.clone(); + const pressedPosition2 = pointer.pressedPosition.clone(); + + // Release the pointer BEFORE asserting, so a failing expectation cannot leak it into later tests. + target.dispatchEvent(generatePointerEvent("pointerleave", 5, left + 6, top + 6, -1, 0)); + engine.update(); + + expect(uniqueID).to.eq(5); + // `position` always tracks the frame's latest event... + expect(position1).to.deep.eq(new Vector2(3 * 2, 3 * 2)); + expect(position2).to.deep.eq(new Vector2(6 * 2, 6 * 2)); + // ...while `pressedPosition` stays at the pointerdown location regardless of later moves. + expect(pressedPosition1).to.deep.eq(new Vector2(1 * 2, 1 * 2)); + expect(pressedPosition2).to.deep.eq(new Vector2(1 * 2, 1 * 2)); + }); + it("keyboard", () => { // @ts-ignore const { _keyboardManager: keyboardManager } = inputManager; @@ -243,6 +278,31 @@ describe("InputManager", async () => { expect(inputManager.wheelDelta).to.deep.eq(new Vector3(1, 2, 3)); }); + it("gc reclaims the pointer event data pool", () => { + // @ts-ignore + const { _pointerManager: pointerManager } = inputManager; + const { _target: target } = pointerManager; + // @ts-ignore + const elements = pointerManager._eventPool._elements; + const { left, top } = target.getBoundingClientRect(); + + // Pressing on the box allocates event data into the pool. + target.dispatchEvent(generatePointerEvent("pointerdown", 6, left + 2, top + 2)); + engine.update(); + expect(elements.length).to.be.greaterThan(0); + + // Engine gc reclaims the pool, releasing the entity references it held. + // @ts-ignore + engine._pendingGC(); + expect(elements.length).to.eq(0); + + // The pool keeps working after gc. + target.dispatchEvent(generatePointerEvent("pointerup", 6, left + 2, top + 2, 0, 0)); + target.dispatchEvent(generatePointerEvent("pointerleave", 6, left + 2, top + 2, -1, 0)); + engine.update(); + expect(elements.length).to.be.greaterThan(0); + }); + it("destroy", () => { engine.destroy(); // @ts-ignore diff --git a/tests/src/ui/UIEvent.test.ts b/tests/src/ui/UIEvent.test.ts index 982173e0a2..102b7323a2 100644 --- a/tests/src/ui/UIEvent.test.ts +++ b/tests/src/ui/UIEvent.test.ts @@ -1,4 +1,4 @@ -import { PointerEventData, Script } from "@galacean/engine-core"; +import { Entity, PointerEventData, Script } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; import { CanvasRenderMode, Image, UICanvas, UITransform } from "@galacean/engine-ui"; import { describe, expect, it } from "vitest"; @@ -35,6 +35,8 @@ describe("UIEvent", async () => { endDragCount = 0; upCount = 0; dropCount = 0; + downTarget: Entity = null; + downCurrentTarget: Entity = null; onPointerEnter(eventData: PointerEventData): void { ++this.enterCount; } @@ -45,6 +47,8 @@ describe("UIEvent", async () => { onPointerDown(eventData: PointerEventData): void { ++this.downCount; + this.downTarget = eventData.target; + this.downCurrentTarget = eventData.currentTarget; } onPointerClick(eventData: PointerEventData): void { @@ -290,6 +294,42 @@ describe("UIEvent", async () => { expect(script3.upCount).toBe(0); expect(script3.dropCount).toBe(0); }); + + it("ui event target and currentTarget", () => { + // @ts-ignore + const { _pointerManager: pointerManager } = inputManager; + const { _target: target } = pointerManager; + const { left, top } = target.getBoundingClientRect(); + + // Re-enable the deepest image (disabled by the previous test) and release the leftover pointer. + image3.enabled = true; + target.dispatchEvent(generatePointerEvent("pointerleave", 2, left + 8, top + 8, -1, 0)); + engine.update(); + + // Reset the records touched by the previous test. + script1.downCount = script2.downCount = script3.downCount = 0; + script1.downTarget = script2.downTarget = script3.downTarget = null; + script1.downCurrentTarget = script2.downCurrentTarget = script3.downCurrentTarget = null; + + // Press at the center so the deepest image (Image3) is hit; the event bubbles Image3 -> Image2 -> Image1. + target.dispatchEvent(generatePointerEvent("pointerdown", 3, left + 8, top + 8)); + engine.update(); + + // Down bubbles through all three nested images. + expect(script1.downCount).toBe(1); + expect(script2.downCount).toBe(1); + expect(script3.downCount).toBe(1); + + // `target` stays the deepest hit entity (Image3) for every handler on the bubble path. + expect(script1.downTarget).toBe(imageEntity3); + expect(script2.downTarget).toBe(imageEntity3); + expect(script3.downTarget).toBe(imageEntity3); + + // `currentTarget` is the entity actually handling the event at each bubble step. + expect(script1.downCurrentTarget).toBe(imageEntity1); + expect(script2.downCurrentTarget).toBe(imageEntity2); + expect(script3.downCurrentTarget).toBe(imageEntity3); + }); }); function generatePointerEvent(