fix(canvas): pan horizontally with Shift+wheel (#523)
- Convert primarily vertical Shift+wheel input to horizontal panning - Preserve native horizontal trackpad deltas instead of swapping both axes - Keep Ctrl and Meta wheel gestures on the existing zoom path
This commit is contained in:
parent
d67415c793
commit
f781c48eaa
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
### Changed
|
||||
|
||||
- Pan horizontally with Shift+wheel while preserving native horizontal trackpad movement.
|
||||
- Move MCP connections into their own Settings destination instead of presenting them as part of model configuration.
|
||||
|
||||
### Performance
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import type { Editor } from '@open-pencil/core/editor'
|
|||
|
||||
import { createRafScheduler } from '#vue/shared/input/raf-scheduler'
|
||||
|
||||
const WHEEL_DELTA_LINE = 1
|
||||
const WHEEL_DELTA_PAGE = 2
|
||||
|
||||
type WheelAccum = {
|
||||
deltaX: number
|
||||
deltaY: number
|
||||
|
|
@ -14,22 +17,37 @@ type WheelAccum = {
|
|||
hasZoom: boolean
|
||||
}
|
||||
|
||||
type WheelPanInput = Pick<WheelEvent, 'deltaX' | 'deltaY' | 'deltaMode' | 'shiftKey'>
|
||||
type WheelModifierInput = Pick<WheelEvent, 'ctrlKey' | 'metaKey'>
|
||||
|
||||
export type WheelPanDelta = { dx: number; dy: number }
|
||||
|
||||
function isMacOs() {
|
||||
return typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)
|
||||
}
|
||||
|
||||
function normalizeWheelDelta(e: WheelEvent): { dx: number; dy: number } {
|
||||
let { deltaX, deltaY } = e
|
||||
if (e.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||
function normalizedWheelDelta(event: WheelPanInput): WheelPanDelta {
|
||||
let { deltaX, deltaY } = event
|
||||
if (event.deltaMode === WHEEL_DELTA_LINE) {
|
||||
deltaX *= 40
|
||||
deltaY *= 40
|
||||
} else if (e.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
} else if (event.deltaMode === WHEEL_DELTA_PAGE) {
|
||||
deltaX *= 800
|
||||
deltaY *= 800
|
||||
}
|
||||
return { dx: deltaX, dy: deltaY }
|
||||
}
|
||||
|
||||
export function isWheelZoom(event: WheelModifierInput): boolean {
|
||||
return event.ctrlKey || event.metaKey
|
||||
}
|
||||
|
||||
export function wheelPanDelta(event: WheelPanInput): WheelPanDelta {
|
||||
const delta = normalizedWheelDelta(event)
|
||||
if (!event.shiftKey || Math.abs(delta.dx) >= Math.abs(delta.dy)) return delta
|
||||
return { dx: delta.dy, dy: 0 }
|
||||
}
|
||||
|
||||
const WHEEL_ZOOM_SPEED = 1.25
|
||||
|
||||
function wheelDeltaModeScale(event: WheelEvent) {
|
||||
|
|
@ -71,18 +89,18 @@ export function setupWheelPanZoom(canvasRef: Ref<HTMLCanvasElement | null>, edit
|
|||
|
||||
const wheelScheduler = createRafScheduler(flushWheel)
|
||||
|
||||
function onWheel(e: WheelEvent) {
|
||||
function onWheel(event: WheelEvent) {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const { dx, dy } = normalizeWheelDelta(e)
|
||||
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (isWheelZoom(event)) {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
wheelAccum.zoomCenterX = e.clientX - rect.left
|
||||
wheelAccum.zoomCenterY = e.clientY - rect.top
|
||||
wheelAccum.zoomScale *= 2 ** wheelZoomDelta(e)
|
||||
wheelAccum.zoomCenterX = event.clientX - rect.left
|
||||
wheelAccum.zoomCenterY = event.clientY - rect.top
|
||||
wheelAccum.zoomScale *= 2 ** wheelZoomDelta(event)
|
||||
wheelAccum.hasZoom = true
|
||||
} else {
|
||||
const { dx, dy } = wheelPanDelta(event)
|
||||
wheelAccum.deltaX -= dx
|
||||
wheelAccum.deltaY -= dy
|
||||
}
|
||||
|
|
|
|||
47
tests/engine/vue/shared/wheel.test.ts
Normal file
47
tests/engine/vue/shared/wheel.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { isWheelZoom, wheelPanDelta } from '#vue/shared/input/wheel'
|
||||
|
||||
function wheelInput(
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
options: { deltaMode?: number; shiftKey?: boolean } = {}
|
||||
) {
|
||||
return {
|
||||
deltaX,
|
||||
deltaY,
|
||||
deltaMode: options.deltaMode ?? 0,
|
||||
shiftKey: options.shiftKey ?? false
|
||||
}
|
||||
}
|
||||
|
||||
describe('wheel navigation', () => {
|
||||
test('preserves ordinary vertical and horizontal movement', () => {
|
||||
expect(wheelPanDelta(wheelInput(0, 24))).toEqual({ dx: 0, dy: 24 })
|
||||
expect(wheelPanDelta(wheelInput(18, 3))).toEqual({ dx: 18, dy: 3 })
|
||||
})
|
||||
|
||||
test('converts a vertical Shift+wheel gesture to horizontal movement', () => {
|
||||
expect(wheelPanDelta(wheelInput(0, 24, { shiftKey: true }))).toEqual({ dx: 24, dy: 0 })
|
||||
expect(wheelPanDelta(wheelInput(2, -16, { shiftKey: true }))).toEqual({ dx: -16, dy: 0 })
|
||||
})
|
||||
|
||||
test('keeps native horizontal Shift+trackpad movement horizontal', () => {
|
||||
expect(wheelPanDelta(wheelInput(20, 2, { shiftKey: true }))).toEqual({ dx: 20, dy: 2 })
|
||||
expect(wheelPanDelta(wheelInput(-12, 0, { shiftKey: true }))).toEqual({ dx: -12, dy: 0 })
|
||||
})
|
||||
|
||||
test('normalizes line and page delta modes before axis mapping', () => {
|
||||
expect(wheelPanDelta(wheelInput(0, 2, { deltaMode: 1, shiftKey: true }))).toEqual({
|
||||
dx: 80,
|
||||
dy: 0
|
||||
})
|
||||
expect(wheelPanDelta(wheelInput(1, 0, { deltaMode: 2 }))).toEqual({ dx: 800, dy: 0 })
|
||||
})
|
||||
|
||||
test('gives Ctrl and Meta zoom precedence independently of Shift', () => {
|
||||
expect(isWheelZoom({ ctrlKey: true, metaKey: false })).toBeTrue()
|
||||
expect(isWheelZoom({ ctrlKey: false, metaKey: true })).toBeTrue()
|
||||
expect(isWheelZoom({ ctrlKey: false, metaKey: false })).toBeFalse()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue