From c874df4d04ae6646ae11ef61e8df7613bf555bbe Mon Sep 17 00:00:00 2001 From: Pierre Bertier Date: Thu, 7 Aug 2025 08:27:46 +0200 Subject: [PATCH] Added trackpad support for moving maps (#1717) ## Description: Added 2-fingers control for map with trackpad ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: pierre_brtr --- src/client/InputHandler.ts | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 3031858df..15d6bee9d 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -175,6 +175,7 @@ export class InputHandler { (e) => { this.onScroll(e); this.onShiftScroll(e); + this.onTrackpadPan(e); e.preventDefault(); }, { passive: false }, @@ -186,6 +187,16 @@ export class InputHandler { this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY)); } }); + + this.canvas.addEventListener("touchstart", (e) => this.onTouchStart(e), { + passive: false, + }); + this.canvas.addEventListener("touchmove", (e) => this.onTouchMove(e), { + passive: false, + }); + this.canvas.addEventListener("touchend", (e) => this.onTouchEnd(e), { + passive: false, + }); this.pointers.clear(); this.moveInterval = setInterval(() => { @@ -402,8 +413,20 @@ export class InputHandler { const realCtrl = this.activeKeys.has("ControlLeft") || this.activeKeys.has("ControlRight"); - const ratio = event.ctrlKey && !realCtrl ? 10 : 1; // Compensate pinch-zoom low sensitivity - this.eventBus.emit(new ZoomEvent(event.x, event.y, event.deltaY * ratio)); + + const isZoomGesture = + event.ctrlKey || + event.metaKey || + Math.abs(event.deltaZ) > 0 || + (event.deltaMode === 1 && Math.abs(event.deltaY) > 0) || + (event.deltaMode === 0 && Math.abs(event.deltaY) >= 50); + + if (isZoomGesture) { + const ratio = event.ctrlKey && !realCtrl ? 10 : 1; + this.eventBus.emit( + new ZoomEvent(event.x, event.y, event.deltaY * ratio), + ); + } } } @@ -415,6 +438,34 @@ export class InputHandler { } } + private onTrackpadPan(event: WheelEvent) { + if (event.shiftKey) { + return; + } + + if (event.ctrlKey || event.metaKey) { + return; + } + + const isTrackpadPan = + event.deltaMode === 0 && + (Math.abs(event.deltaX) > 0 || Math.abs(event.deltaY) > 0) && + ((Math.abs(event.deltaX) > 0 && Math.abs(event.deltaY) > 0) || + event.deltaX % 1 !== 0 || + event.deltaY % 1 !== 0 || + (Math.abs(event.deltaX) < 30 && Math.abs(event.deltaY) < 30)); + + if (isTrackpadPan) { + const panSensitivity = 1.0; + const deltaX = -event.deltaX * panSensitivity; + const deltaY = -event.deltaY * panSensitivity; + + if (Math.abs(deltaX) > 0.5 || Math.abs(deltaY) > 0.5) { + this.eventBus.emit(new DragEvent(deltaX, deltaY)); + } + } + } + private onPointerMove(event: PointerEvent) { if (event.button === 1) { event.preventDefault(); @@ -459,6 +510,42 @@ export class InputHandler { this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY)); } + private onTouchStart(event: TouchEvent) { + if (event.touches.length === 2) { + event.preventDefault(); + } + } + + private onTouchMove(event: TouchEvent) { + if (event.touches.length === 2) { + event.preventDefault(); + + const touch1 = event.touches[0]; + const touch2 = event.touches[1]; + const centerX = (touch1.clientX + touch2.clientX) / 2; + const centerY = (touch1.clientY + touch2.clientY) / 2; + + if (this.lastPointerX !== 0 && this.lastPointerY !== 0) { + const deltaX = centerX - this.lastPointerX; + const deltaY = centerY - this.lastPointerY; + + if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { + this.eventBus.emit(new DragEvent(deltaX, deltaY)); + } + } + + this.lastPointerX = centerX; + this.lastPointerY = centerY; + } + } + + private onTouchEnd(event: TouchEvent) { + if (event.touches.length < 2) { + this.lastPointerX = 0; + this.lastPointerY = 0; + } + } + private getPinchDistance(): number { const pointerEvents = Array.from(this.pointers.values()); const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;