From e14f866144abf4525b0ba8e7dc87a9843c3ee070 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 2 Nov 2024 22:55:25 -0500 Subject: [PATCH] fix: [#3078] Pointer lastWorldPosition updates when the camera does (#3245) Closes #3078 --- CHANGELOG.md | 1 + src/engine/Camera.ts | 21 +++++++++++++----- src/engine/Input/PointerAbstraction.ts | 13 ++++++++++++ src/engine/Input/PointerEventReceiver.ts | 7 ++++++ src/engine/Input/PointerSystem.ts | 9 +++++++- src/engine/Math/watch-vector.ts | 5 +++++ src/spec/CameraSpec.ts | 2 +- src/spec/PointerInputSpec.ts | 27 ++++++++++++++++++++++-- 8 files changed, 76 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a45c911..bbba2a915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,7 @@ are doing mtv adjustments during precollision. ### Fixed +- Fixed issue where the pointer `lastWorldPos` was not updated when the current `Camera` moved - Fixed issue where `cancel()`'d events still bubbled to the top level input handlers - Fixed issue where unexpected html HTML content from an image would silently hang the loader - Fixed issue where Collision events ahd inconsistent targets, sometimes they were Colliders and sometimes they were Entities diff --git a/src/engine/Camera.ts b/src/engine/Camera.ts index 97608c31d..d84745218 100644 --- a/src/engine/Camera.ts +++ b/src/engine/Camera.ts @@ -9,11 +9,11 @@ import { PreUpdateEvent, PostUpdateEvent, InitializeEvent } from './Events'; import { BoundingBox } from './Collision/BoundingBox'; import { Logger } from './Util/Log'; import { ExcaliburGraphicsContext } from './Graphics/Context/ExcaliburGraphicsContext'; -import { watchAny } from './Util/Watch'; import { AffineMatrix } from './Math/affine-matrix'; import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter'; import { pixelSnapEpsilon } from './Graphics'; import { sign } from './Math/util'; +import { WatchVector } from './Math/watch-vector'; /** * Interface that describes a custom camera strategy for tracking targets @@ -312,17 +312,28 @@ export class Camera implements CanUpdate, CanInitialize { this._angularVelocity = value; } + private _posChanged = false; + private _pos: Vector = new WatchVector(Vector.Zero, () => { + this._posChanged = true; + }); /** * Get or set the camera's position */ - private _posChanged = false; - private _pos: Vector = watchAny(Vector.Zero, () => (this._posChanged = true)); public get pos(): Vector { return this._pos; } public set pos(vec: Vector) { - this._pos = watchAny(vec, () => (this._posChanged = true)); this._posChanged = true; + this._pos = new WatchVector(vec, () => { + this._posChanged = true; + }); + } + + /** + * Has the position changed since the last update + */ + public hasChanged(): boolean { + return this._posChanged; } /** * Interpolated camera position if more draws are running than updates @@ -782,8 +793,8 @@ export class Camera implements CanUpdate, CanInitialize { // It's important to update the camera after strategies // This prevents jitter this.updateTransform(this.pos); - this._postupdate(engine, elapsedMs); + this._posChanged = false; } private _snapPos = vec(0, 0); diff --git a/src/engine/Input/PointerAbstraction.ts b/src/engine/Input/PointerAbstraction.ts index ba298c0b6..11cde1fae 100644 --- a/src/engine/Input/PointerAbstraction.ts +++ b/src/engine/Input/PointerAbstraction.ts @@ -2,6 +2,8 @@ import { Vector } from '../Math/vector'; import { PointerEvent } from './PointerEvent'; import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter'; import { PointerEvents } from './PointerEventReceiver'; +import { GlobalCoordinates } from '../Math/global-coordinates'; +import { Engine } from '../Engine'; export class PointerAbstraction { public events = new EventEmitter(); @@ -50,6 +52,17 @@ export class PointerAbstraction { this.events.off(eventName, handler); } + /** + * Called internally by excalibur to keep pointers up to date + * @internal + * @param pos + */ + public _updateWorldPosition(engine: Engine) { + const coord = GlobalCoordinates.fromPagePosition(this.lastPagePos, engine); + this.lastScreenPos = coord.screenPos; + this.lastWorldPos = coord.worldPos; + } + private _onPointerMove = (ev: PointerEvent): void => { this.lastPagePos = new Vector(ev.pagePos.x, ev.pagePos.y); this.lastScreenPos = new Vector(ev.screenPos.x, ev.screenPos.y); diff --git a/src/engine/Input/PointerEventReceiver.ts b/src/engine/Input/PointerEventReceiver.ts index 9827e33f3..338c1730c 100644 --- a/src/engine/Input/PointerEventReceiver.ts +++ b/src/engine/Input/PointerEventReceiver.ts @@ -202,6 +202,7 @@ export class PointerEventReceiver { * Updates the current frame pointer info and emits raw pointer events * * This does not emit events to entities, see PointerSystem + * @internal */ public update() { this.lastFramePointerDown = new Map(this.currentFramePointerDown); @@ -253,6 +254,12 @@ export class PointerEventReceiver { this.primary.emit('pointerwheel', event); this.primary.emit('wheel', event); } + + if (this.engine.currentScene.camera.hasChanged()) { + for (const pointer of this._pointers) { + pointer._updateWorldPosition(this.engine); + } + } } /** diff --git a/src/engine/Input/PointerSystem.ts b/src/engine/Input/PointerSystem.ts index 506241a86..3c9220056 100644 --- a/src/engine/Input/PointerSystem.ts +++ b/src/engine/Input/PointerSystem.ts @@ -95,6 +95,11 @@ export class PointerSystem extends System { }; public preupdate(): void { + if (this._scene.camera.hasChanged()) { + // if the camera has changed we want to force a transform update so pointers can be correctly calc'd + this._scene.camera.updateTransform(this._scene.camera.pos); + } + // event receiver might change per frame this._receivers = [this._engine.input.pointers, this._scene.input.pointers]; this._engineReceiver = this._engine.input.pointers; @@ -142,8 +147,10 @@ export class PointerSystem extends System { // Dispatch pointer events on entities this._dispatchEvents(this._sortedEntities); - // Clear last frame's events + // Dispatch pointer events on top level pointers this._receivers.forEach((r) => r.update()); + + // Clear last frame's events this.lastFrameEntityToPointers.clear(); this.lastFrameEntityToPointers = new Map(this.currentFrameEntityToPointers); this.currentFrameEntityToPointers.clear(); diff --git a/src/engine/Math/watch-vector.ts b/src/engine/Math/watch-vector.ts index 1e43ab76a..ffdc5f24b 100644 --- a/src/engine/Math/watch-vector.ts +++ b/src/engine/Math/watch-vector.ts @@ -31,4 +31,9 @@ export class WatchVector extends Vector { this._y = this.original.y = newY; } } + + override setTo(x: number, y: number): void { + this.x = x; + this.y = y; + } } diff --git a/src/spec/CameraSpec.ts b/src/spec/CameraSpec.ts index b26734d3d..7cef62e27 100644 --- a/src/spec/CameraSpec.ts +++ b/src/spec/CameraSpec.ts @@ -325,7 +325,7 @@ describe('A camera', () => { it('can use built-in elastic around actor strategy', () => { engine.currentScene.camera = new ex.Camera(); - engine.currentScene.camera.pos.setTo(0, 0); + engine.currentScene.camera.pos = ex.vec(0, 0); const actor = new ex.Actor(); engine.currentScene.camera.strategy.elasticToActor(actor, 0.05, 0.1); diff --git a/src/spec/PointerInputSpec.ts b/src/spec/PointerInputSpec.ts index bdd415a6e..f830005a9 100644 --- a/src/spec/PointerInputSpec.ts +++ b/src/spec/PointerInputSpec.ts @@ -229,6 +229,29 @@ describe('A pointer', () => { expect(spyTopLevelPointerWheel).not.toHaveBeenCalled(); }); + it('should update the pointer pos when the camera moves', () => { + const oldMargin = document.body.style.margin; + const oldPadding = document.body.style.padding; + document.body.style.margin = '0'; + document.body.style.padding = '0'; + executeMouseEvent('pointerdown', document, null, 10, 10); + engine.currentScene.update(engine, 0); + expect(engine.input.pointers.primary.lastWorldPos).toEqual(ex.vec(10, 10)); + expect(engine.input.pointers.primary.lastScreenPos).toEqual(ex.vec(10, 10)); + expect(engine.input.pointers.primary.lastPagePos).toEqual(ex.vec(10, 10)); + + expect(engine.currentScene.camera.hasChanged()).toBe(false); + engine.currentScene.camera.pos = ex.vec(1000, 1000); + expect(engine.currentScene.camera.hasChanged()).toBe(true); + engine.currentScene.update(engine, 0); + expect(engine.input.pointers.primary.lastWorldPos).toEqual(ex.vec(760, 760)); + expect(engine.input.pointers.primary.lastScreenPos).toEqual(ex.vec(10, 10)); + expect(engine.input.pointers.primary.lastPagePos).toEqual(ex.vec(10, 10)); + + document.body.style.margin = oldMargin; + document.body.style.padding = oldPadding; + }); + it('should dispatch point events on screen elements', () => { const pointerDownSpy = jasmine.createSpy('pointerdown'); const screenElement = new ex.ScreenElement({ @@ -307,9 +330,9 @@ describe('A pointer', () => { const actor1 = new ex.Actor({ pos: ex.vec(50, 50), width: 100, - height: 100 + height: 100, + coordPlane: ex.CoordPlane.Screen }); - actor1.transform.coordPlane = ex.CoordPlane.Screen; actor1.on('pointerdown', clickSpy); engine.currentScene.camera.pos = ex.vec(1000, 1000); engine.currentScene.camera.draw(engine.graphicsContext);