From eda1f2a79afb754b1ff0eff5ca3c2af43c706e1e Mon Sep 17 00:00:00 2001 From: xigua Date: Tue, 16 Jul 2024 21:12:02 +0800 Subject: [PATCH] feat: mouse event manager --- packages/core/src/canvas_dragger.ts | 77 ++++--- packages/core/src/editor.ts | 4 +- .../host_event_manager/host_event_manager.ts | 55 +---- packages/core/src/host_event_manager/index.ts | 1 + .../host_event_manager/mouse_event_manager.ts | 199 ++++++++++++++++++ .../core/src/tools/tool_select/tool_select.ts | 9 +- .../src/components/DebugPanel/DebugPanel.tsx | 13 +- 7 files changed, 257 insertions(+), 101 deletions(-) create mode 100644 packages/core/src/host_event_manager/mouse_event_manager.ts diff --git a/packages/core/src/canvas_dragger.ts b/packages/core/src/canvas_dragger.ts index a4a0468d..73b38c38 100644 --- a/packages/core/src/canvas_dragger.ts +++ b/packages/core/src/canvas_dragger.ts @@ -2,6 +2,7 @@ import { EventEmitter } from '@suika/common'; import { type IPoint } from '@suika/geo'; import { type SuikaEditor } from './editor'; +import { type IMouseEvent, type IMousemoveEvent } from './host_event_manager'; interface Events { activeChange(active: boolean): void; @@ -17,8 +18,7 @@ export class CanvasDragger { private isEnableDragCanvasBySpace = true; private _isPressing = false; - private isDragging = false; - private startPoint: IPoint = { x: 0, y: 0 }; + private startVwPos: IPoint = { x: 0, y: 0 }; private startViewportPos: IPoint = { x: 0, y: 0 }; private eventEmitter = new EventEmitter(); @@ -47,7 +47,7 @@ export class CanvasDragger { constructor(private editor: SuikaEditor) { this.editor.hostEventManager.on('spaceToggle', this.handleSpaceToggle); - this.editor.hostEventManager.on( + this.editor.mouseEventManager.on( 'wheelBtnToggle', this.handleWheelBtnToggle, ); @@ -69,20 +69,18 @@ export class CanvasDragger { this.eventEmitter.emit('activeChange', true); this._active = true; this.editor.setCursor('grab'); - this.bindEvent(); + this.bindEventWhenActive(); if (event) { this.editor.setCursor('grabbing'); - this.handlePointerDown(event); + const vwPos = this.editor.getCursorXY(event); + this.onStart({ + pos: this.editor.viewportCoordsToScene(vwPos.x, vwPos.y), + vwPos, + nativeEvent: event, + }); } } - private bindEvent() { - const canvas = this.editor.canvasElement; - canvas.addEventListener('pointerdown', this.handlePointerDown); - window.addEventListener('pointermove', this.handlePointerMove); - window.addEventListener('pointerup', this.handlePointerUp); - } - inactive() { if (this._isPressing) { this.inactiveAfterPointerUp = true; @@ -93,18 +91,11 @@ export class CanvasDragger { } this.eventEmitter.emit('activeChange', false); this._active = false; - this.unbindEvent(); + this.unbindEventWhenInactive(); + this.editor.toolManager.setCursorWhenActive(); } } - private unbindEvent() { - const canvas = this.editor.canvasElement; - canvas.removeEventListener('pointerdown', this.handlePointerDown); - window.removeEventListener('pointermove', this.handlePointerMove); - window.removeEventListener('pointerup', this.handlePointerUp); - this.editor.toolManager.setCursorWhenActive(); - } - enableDragBySpace() { this.isEnableDragCanvasBySpace = true; } @@ -112,31 +103,26 @@ export class CanvasDragger { this.isEnableDragCanvasBySpace = false; } - private handlePointerDown = (e: PointerEvent) => { - if (!(e.button === 0 || e.button === 1)) return; + private onStart = (event: IMouseEvent) => { + if (event.nativeEvent.button !== 0 && event.nativeEvent.button !== 1) { + return; + } this.editor.cursorManager.setCursor('grabbing'); this._isPressing = true; - this.startPoint = this.editor.getCursorXY(e); + this.startVwPos = { ...event.vwPos }; this.startViewportPos = this.editor.viewportManager.getViewport(); }; - private handlePointerMove = (e: PointerEvent) => { + private onDrag = (event: IMousemoveEvent) => { if (!this._isPressing) return; - const point: IPoint = this.editor.getCursorXY(e); - const dx = point.x - this.startPoint.x; - const dy = point.y - this.startPoint.y; + const dx = event.vwPos.x - this.startVwPos.x; + const dy = event.vwPos.y - this.startVwPos.y; - const zoom = this.editor.zoomManager.getZoom(); const dragBlockStep = this.editor.setting.get('dragBlockStep'); - if ( - !this.isDragging && - (Math.abs(dx) > dragBlockStep || Math.abs(dy) > dragBlockStep) - ) { - this.isDragging = true; - } - if (this.isDragging) { + const zoom = this.editor.zoomManager.getZoom(); + if (event.maxDragDistance > dragBlockStep / zoom) { const viewportX = this.startViewportPos.x - dx / zoom; const viewportY = this.startViewportPos.y - dy / zoom; this.editor.viewportManager.setViewport({ x: viewportX, y: viewportY }); @@ -144,9 +130,8 @@ export class CanvasDragger { } }; - private handlePointerUp = () => { + private onEnd = () => { this.editor.cursorManager.setCursor('grab'); - this.isDragging = false; this._isPressing = false; if (this.inactiveAfterPointerUp) { this.inactiveAfterPointerUp = false; @@ -154,13 +139,25 @@ export class CanvasDragger { } }; + private bindEventWhenActive() { + this.editor.mouseEventManager.on('start', this.onStart); + this.editor.mouseEventManager.on('drag', this.onDrag); + this.editor.mouseEventManager.on('end', this.onEnd); + } + + private unbindEventWhenInactive() { + this.editor.mouseEventManager.off('start', this.onStart); + this.editor.mouseEventManager.off('drag', this.onDrag); + this.editor.mouseEventManager.off('end', this.onEnd); + } + destroy() { this.editor.hostEventManager.off('spaceToggle', this.handleSpaceToggle); - this.editor.hostEventManager.off( + this.editor.mouseEventManager.off( 'wheelBtnToggle', this.handleWheelBtnToggle, ); - this.unbindEvent(); + this.unbindEventWhenInactive(); } on(eventName: K, handler: Events[K]) { diff --git a/packages/core/src/editor.ts b/packages/core/src/editor.ts index 8cc38a1e..9af00648 100644 --- a/packages/core/src/editor.ts +++ b/packages/core/src/editor.ts @@ -14,7 +14,7 @@ import { CursorManger, type ICursor } from './cursor_manager'; import { type GraphicsAttrs } from './graphs'; import { SuikaCanvas } from './graphs/canvas'; import { SuikaDocument } from './graphs/document'; -import { HostEventManager } from './host_event_manager'; +import { HostEventManager, MouseEventManager } from './host_event_manager'; import { ImgManager } from './Img_manager'; import { KeyBindingManager } from './key_binding_manager'; import { PathEditor } from './path_editor'; @@ -69,6 +69,7 @@ export class SuikaEditor { imgManager: ImgManager; cursorManager: CursorManger; + mouseEventManager: MouseEventManager; keybindingManager: KeyBindingManager; hostEventManager: HostEventManager; clipboard: ClipboardManager; @@ -96,6 +97,7 @@ export class SuikaEditor { this.setting.set('offsetY', options.offsetY); } + this.mouseEventManager = new MouseEventManager(this); this.keybindingManager = new KeyBindingManager(this); this.keybindingManager.bindEvent(); diff --git a/packages/core/src/host_event_manager/host_event_manager.ts b/packages/core/src/host_event_manager/host_event_manager.ts index 50e72d18..d651afab 100644 --- a/packages/core/src/host_event_manager/host_event_manager.ts +++ b/packages/core/src/host_event_manager/host_event_manager.ts @@ -1,5 +1,5 @@ import { EventEmitter } from '@suika/common'; -import { distance, type IPoint } from '@suika/geo'; +import { type IPoint } from '@suika/geo'; import { type SuikaEditor } from '../editor'; import { CommandKeyBinding } from './command_key_binding'; @@ -9,9 +9,7 @@ interface Events { shiftToggle(press: boolean): void; altToggle(press: boolean): void; spaceToggle(press: boolean): void; - wheelBtnToggle(press: boolean, event: PointerEvent): void; contextmenu(point: IPoint): void; - continueClick(): void; } /** @@ -43,7 +41,6 @@ export class HostEventManager { bindHotkeys() { this.bindModifiersRecordEvent(); // 记录 isShiftPressing 等值 this.bindWheelEvent(); - this.bindMouseRecordEvent(); this.bindContextMenu(); this.moveGraphsKeyBinding.bindKey(); @@ -84,56 +81,6 @@ export class HostEventManager { }); } - private bindMouseRecordEvent() { - let pointerDownTimeStamp = -Infinity; - let lastPointerDownPos: IPoint = { x: -99, y: -99 }; - - const handlePointerEvent = (event: PointerEvent) => { - // mouse left - if (event.button === 0 && event.type === 'pointerdown') { - const now = new Date().getTime(); - const newPos = { - x: event.pageX, - y: event.pageY, - }; - - const interval = now - pointerDownTimeStamp; - const clickDistanceDiff = distance(newPos, lastPointerDownPos); - if ( - interval < this.editor.setting.get('continueClickMaxGap') && - clickDistanceDiff < - this.editor.setting.get('continueClickDistanceTol') - ) { - pointerDownTimeStamp = now; - this.eventEmitter.emit('continueClick'); - } - pointerDownTimeStamp = now; - lastPointerDownPos = newPos; - } - - // mouse middle - if (event.button === 1) { - const prevWheelBtnPressing = this.isWheelBtnPressing; - this.isWheelBtnPressing = event.type === 'pointerdown'; - if (prevWheelBtnPressing !== this.isWheelBtnPressing) { - this.eventEmitter.emit( - 'wheelBtnToggle', - this.isWheelBtnPressing, - event, - ); - } - } - }; - - document.addEventListener('pointerdown', handlePointerEvent); - document.addEventListener('pointerup', handlePointerEvent); - - this.unbindHandlers.push(() => { - document.removeEventListener('pointerdown', handlePointerEvent); - document.removeEventListener('pointerup', handlePointerEvent); - }); - } - /** * shiftToggle 会在切换时触发。按住 shift 不放,只会触发一次 */ diff --git a/packages/core/src/host_event_manager/index.ts b/packages/core/src/host_event_manager/index.ts index a17cb2d9..d1130dec 100644 --- a/packages/core/src/host_event_manager/index.ts +++ b/packages/core/src/host_event_manager/index.ts @@ -1 +1,2 @@ export * from './host_event_manager'; +export * from './mouse_event_manager'; diff --git a/packages/core/src/host_event_manager/mouse_event_manager.ts b/packages/core/src/host_event_manager/mouse_event_manager.ts new file mode 100644 index 00000000..59da9b5a --- /dev/null +++ b/packages/core/src/host_event_manager/mouse_event_manager.ts @@ -0,0 +1,199 @@ +import { cloneDeep, EventEmitter, isEqual } from '@suika/common'; +import { distance, type IPoint } from '@suika/geo'; + +import { type SuikaEditor } from '../editor'; + +enum MouseKey { + Left = 0, + Mid = 1, +} + +export type IMouseEvent = Readonly<{ + pos: Readonly; + vwPos: Readonly; + nativeEvent: PointerEvent; +}>; + +export type IMousemoveEvent = Readonly<{ + pos: Readonly; + vwPos: Readonly; + nativeEvent: PointerEvent; + maxDragDistance: number; + isOutside: boolean; // dragging state and mouse is outside canvas +}>; + +interface Events { + wheelBtnToggle(press: boolean, event: PointerEvent): void; + cursorPosUpdate(pos: IPoint | null): void; + start(event: IMouseEvent): void; + end(event: IMouseEvent): void; + move(event: IMousemoveEvent): void; + drag(event: IMousemoveEvent): void; + comboClick(event: IMouseEvent): void; +} + +export class MouseEventManager { + private isWheelBtnPressing = false; + private eventEmitter = new EventEmitter(); + private cursorPos: IPoint | null = null; + + constructor(private editor: SuikaEditor) { + this.bindEvent(); + } + + getCursorPos() { + return cloneDeep(this.cursorPos); + } + + private setCursorPos(pos: IPoint | null) { + const prevCursorPos = this.cursorPos; + this.cursorPos = pos && { x: pos.x, y: pos.y }; + + if (!isEqual(prevCursorPos, this.cursorPos)) { + this.eventEmitter.emit('cursorPosUpdate', cloneDeep(pos)); + } + } + + private startPos: IPoint = { x: 0, y: 0 }; + private isPressing = false; + private maxDragDistance = 0; + + private onPointerdown = (event: PointerEvent) => { + if (event.target !== this.editor.canvasElement) { + return; + } + this.updateIsWheelBtnPressing(event); + this.isPressing = true; + + const { pos, vwPos } = this.getPosAndVwPos(event); + this.startPos = { ...pos }; + const e = { + pos, + vwPos, + nativeEvent: event, + }; + this.eventEmitter.emit('start', e); + this.handleComboClick(e); + }; + + private onPointerMove = (event: PointerEvent) => { + const isInsideCanvas = event.target === this.editor.canvasElement; + if (isInsideCanvas || this.isPressing) { + const cursorPos = this.editor.getSceneCursorXY(event); + this.setCursorPos(cursorPos); + } + + const { pos, vwPos } = this.getPosAndVwPos(event); + if (this.isPressing) { + const dx = pos.x - this.startPos.x; + const dy = pos.y - this.startPos.y; + const dragDistance = Math.max(Math.abs(dx), Math.abs(dy)); + this.maxDragDistance = Math.max(dragDistance, this.maxDragDistance); + this.eventEmitter.emit('drag', { + pos, + vwPos, + nativeEvent: event, + isOutside: isInsideCanvas, + maxDragDistance: this.maxDragDistance, + }); + } else { + this.eventEmitter.emit('move', { + pos, + vwPos, + nativeEvent: event, + isOutside: isInsideCanvas, + maxDragDistance: this.maxDragDistance, + }); + } + }; + + private getPosAndVwPos(event: PointerEvent) { + const vwPos = this.editor.getCursorXY(event); + return { + pos: this.editor.viewportCoordsToScene(vwPos.x, vwPos.y), + vwPos, + }; + } + + private onPointerUp = (event: PointerEvent) => { + this.updateIsWheelBtnPressing(event); + this.isPressing = false; + this.maxDragDistance = 0; + const isInsideCanvas = event.target === this.editor.canvasElement; + + if (isInsideCanvas || this.isPressing) { + this.eventEmitter.emit('end', { + ...this.getPosAndVwPos(event), + nativeEvent: event, + }); + } + }; + + private updateIsWheelBtnPressing(event: PointerEvent) { + if (event.button === MouseKey.Mid) { + const prevWheelBtnPressing = this.isWheelBtnPressing; + if (event.type === 'pointerdown') { + this.isWheelBtnPressing = true; + } else if (event.type === 'pointerup') { + this.isWheelBtnPressing = false; + } + if (prevWheelBtnPressing !== this.isWheelBtnPressing) { + this.eventEmitter.emit( + 'wheelBtnToggle', + this.isWheelBtnPressing, + event, + ); + } + } + } + + private pointerDownTimeStamp = -Infinity; + private lastPointerDownPos: IPoint = { x: -99, y: -99 }; + + private handleComboClick = (event: IMouseEvent) => { + const nativeEvent = event.nativeEvent; + if (nativeEvent.button !== MouseKey.Left) { + return; + } + const now = new Date().getTime(); + const newPos = { + x: nativeEvent.pageX, + y: nativeEvent.pageY, + }; + + const interval = now - this.pointerDownTimeStamp; + const clickDistanceDiff = distance(newPos, this.lastPointerDownPos); + if ( + interval < this.editor.setting.get('continueClickMaxGap') && + clickDistanceDiff < this.editor.setting.get('continueClickDistanceTol') + ) { + this.pointerDownTimeStamp = now; + this.eventEmitter.emit('comboClick', event); + } + this.pointerDownTimeStamp = now; + this.lastPointerDownPos = newPos; + }; + + private bindEvent() { + window.addEventListener('pointerdown', this.onPointerdown); + window.addEventListener('pointermove', this.onPointerMove); + window.addEventListener('pointerup', this.onPointerUp); + } + + private unbindEvent() { + window.removeEventListener('pointerdown', this.onPointerdown); + window.removeEventListener('pointermove', this.onPointerMove); + window.removeEventListener('pointerup', this.onPointerUp); + } + + destroy() { + this.unbindEvent(); + } + + on(eventName: K, handler: Events[K]) { + this.eventEmitter.on(eventName, handler); + } + off(eventName: K, handler: Events[K]) { + this.eventEmitter.off(eventName, handler); + } +} diff --git a/packages/core/src/tools/tool_select/tool_select.ts b/packages/core/src/tools/tool_select/tool_select.ts index 47251675..587e6f41 100644 --- a/packages/core/src/tools/tool_select/tool_select.ts +++ b/packages/core/src/tools/tool_select/tool_select.ts @@ -9,6 +9,7 @@ import { SuikaPath, SuikaText, } from '../../graphs'; +import { type IMouseEvent } from '../../host_event_manager'; import { type IBaseTool, type ITool } from '../type'; import { SelectMoveTool } from './tool_select_move'; import { SelectResizeTool } from './tool_select_resize'; @@ -55,8 +56,8 @@ export class SelectTool implements ITool { }; // double click to active path editor - private onContinueClick = () => { - const point = this.editor.toolManager.getCurrPoint(); + private onContinueClick = (event: IMouseEvent) => { + const point = event.pos; const editor = this.editor; const handleInfo = editor.controlHandleManager.getHandleInfoByPoint(point); if (handleInfo) return; @@ -92,14 +93,14 @@ export class SelectTool implements ITool { 'hoverItemChange', this.handleHoverItemChange, ); - this.editor.hostEventManager.on('continueClick', this.onContinueClick); + this.editor.mouseEventManager.on('comboClick', this.onContinueClick); } onInactive() { this.editor.selectedElements.off( 'hoverItemChange', this.handleHoverItemChange, ); - this.editor.hostEventManager.off('continueClick', this.onContinueClick); + this.editor.mouseEventManager.off('comboClick', this.onContinueClick); this.editor.render(); } diff --git a/packages/suika/src/components/DebugPanel/DebugPanel.tsx b/packages/suika/src/components/DebugPanel/DebugPanel.tsx index 01ed0105..25f6621f 100644 --- a/packages/suika/src/components/DebugPanel/DebugPanel.tsx +++ b/packages/suika/src/components/DebugPanel/DebugPanel.tsx @@ -1,4 +1,5 @@ import { type SuikaGraphics } from '@suika/core'; +import { type IPoint } from '@suika/geo'; import { type FC, useContext, useEffect, useState } from 'react'; import { EditorContext } from '../../context'; @@ -8,6 +9,7 @@ export const DebugPanel: FC = () => { const [isSelectedBoxHover, setIsSelectedBoxHover] = useState(false); const [hoveredGraphName, setHoveredGraphName] = useState(''); + const [cursorPos, setCursorPos] = useState(null); useEffect(() => { if (!editor) return; @@ -15,12 +17,13 @@ export const DebugPanel: FC = () => { const handleSelectedBoxHover = (isHover: boolean) => { setIsSelectedBoxHover(isHover); }; - editor.selectedBox.on('hoverChange', handleSelectedBoxHover); - const handleHoverItemChange = (hoveredItem: SuikaGraphics | null) => { setHoveredGraphName(hoveredItem?.attrs?.objectName ?? ''); }; + + editor.selectedBox.on('hoverChange', handleSelectedBoxHover); editor.selectedElements.on('hoverItemChange', handleHoverItemChange); + editor.mouseEventManager.on('cursorPosUpdate', setCursorPos); return () => { editor.selectedBox.off('hoverChange', handleSelectedBoxHover); @@ -32,6 +35,12 @@ export const DebugPanel: FC = () => {
isSelectedBoxHover: {isSelectedBoxHover ? 'true' : 'false'}
hoveredGraphName: {hoveredGraphName}
+ {cursorPos && ( + <> +
X: {cursorPos.x.toFixed(2)}
+
Y: {cursorPos.y.toFixed(2)}
+ + )}
); };