import { distance, Point, Region } from "@noya-app/noya-geometry"; import { isLeftButtonClicked, isRightButtonClicked } from "../utils/mouseUtils"; import { createCanvasPlugin } from "./baseCanvas"; import { BaseContextProperties, PointerCaptureProperties, ScreenPointProperties, } from "./contextProperties"; import { NoneMode } from "./PluginSystem"; type PointerMode = NoneMode; export type PointerData = { pointer?: Point; pointerDown?: Region; clickCount?: number; lastClickTime?: number; lastClickScreenPoint?: Point; lastRightClickCanvasPoint?: Point; // Non-adjusted pointer position rawPointer?: Point; }; export interface UniquePointerContextProperties { adjustPoint?: (event: React.PointerEvent, point: Point) => Point; completedPointer?: ( event: React.PointerEvent, region: Region, clickCount: number ) => void; } export interface PointerContextProperties extends BaseContextProperties, ScreenPointProperties, PointerCaptureProperties, UniquePointerContextProperties {} type PointerPluginOptions = { clickTimeThreshold?: number; }; export function pointerPlugin(options: PointerPluginOptions = {}) { const { clickTimeThreshold = 300 } = options; return createCanvasPlugin({ name: "pointer", data: { clickCount: 0, lastClickTime: 0 }, handlers: ({ getMode, setMode, getData, setData, adjustPoint = (_, point) => point, ...context }: PointerContextProperties) => ({ onPointerDown(event) { const data = getData(); if (isRightButtonClicked(event)) { setData({ ...data, lastRightClickCanvasPoint: context.getCanvasPoint( event.nativeEvent ), }); return; } if (!isLeftButtonClicked(event)) return; const screenPoint = context.getScreenPoint(event.nativeEvent); const rawEnd = context.getCanvasPoint(event.nativeEvent); const end = adjustPoint(event, rawEnd); const currentTime = Date.now(); let clickCount = data.clickCount ?? 0; if ( currentTime - (data.lastClickTime ?? 0) < clickTimeThreshold && data.lastClickScreenPoint && distance(data.lastClickScreenPoint, screenPoint) < 2 ) { clickCount += 1; } else { clickCount = 1; } setData({ ...data, pointerDown: { anchor: end, head: end }, pointer: end, rawPointer: rawEnd, clickCount, lastClickTime: currentTime, lastClickScreenPoint: screenPoint, }); context.setPointerCapture(event.pointerId); }, onPointerMove(event) { const data = getData(); const rawEnd = context.getCanvasPoint(event.nativeEvent); const end = adjustPoint(event, rawEnd); if (data.pointerDown) { setData({ ...data, pointerDown: { ...data.pointerDown, head: end }, pointer: end, rawPointer: rawEnd, }); } else { setData({ ...data, pointer: end, rawPointer: rawEnd }); } }, onPointerUp(event) { if (!isLeftButtonClicked(event)) return; const data = getData(); const rawEnd = context.getCanvasPoint(event.nativeEvent); const end = adjustPoint(event, rawEnd); if (data.pointerDown) { setData({ ...data, pointerDown: { ...data.pointerDown, head: end }, rawPointer: rawEnd, }); context.completedPointer?.( event, { ...data.pointerDown, head: end }, data.clickCount ?? 0 ); context.releasePointerCapture(event.pointerId); } else { setData({ ...data, pointer: end, rawPointer: rawEnd }); } }, onPointerLeave() { const data = getData(); setData({ ...data, pointer: undefined, rawPointer: undefined }); }, onPointerCancel() { const data = getData(); setData({ ...data, pointerDown: undefined, pointer: undefined, rawPointer: undefined, }); }, }), finalizers: (context) => ({ onPointerUp() { const data = context.getData(); if (!data.pointerDown) return; context.setData({ ...data, pointerDown: undefined, rawPointer: undefined, }); }, }), }); }