import { Point, Region } from "@noya-app/noya-geometry"; import { uuid } from "@noya-app/noya-utils"; import { DEFAULT_MOVEMENT_THRESHOLD, isMoving } from "../utils/isMoving"; import { IdentifiablePoint } from "../utils/pointUtils"; import { createCanvasPlugin } from "./baseCanvas"; import { BaseContextProperties, ElementProperties, SelectableProperties, } from "./contextProperties"; import { BaseMode, NoneMode, modeSwitch } from "./PluginSystem"; import { PointerData } from "./pointerPlugin"; type DrawingMode = NoneMode | { type: "drawing"; region: Region }; type DrawingData = { zoom: number; drawnPoints: IdentifiablePoint[]; } & Pick; interface DrawingHandlerContext extends BaseContextProperties, ElementProperties, SelectableProperties { canDraw?: (event: React.PointerEvent) => boolean; adjustDrawingPoint?: (event: React.PointerEvent, point: Point) => Point; adjustDrawingRegion?: (event: React.PointerEvent, region: Region) => Region; addDrawnPoint?: (event: React.PointerEvent, point: IdentifiablePoint) => void; addDrawnElement?: ( event: React.PointerEvent, region: Region ) => BaseMode | undefined; } type DrawingPluginOptions = { movementThreshold?: number; }; export function drawingPlugin( options: DrawingPluginOptions = {} ) { const { movementThreshold = DEFAULT_MOVEMENT_THRESHOLD } = options; return createCanvasPlugin({ name: "drawing", data: { zoom: 1, drawnPoints: [], }, handlers: ({ getMode, setMode, getData, setData, canDraw = () => getMode().type === "none", adjustDrawingPoint = (_, point) => point, adjustDrawingRegion = (_, region) => region, addDrawnPoint = () => {}, addDrawnElement = () => undefined, }: DrawingHandlerContext) => ({ onPointerDown(event) { modeSwitch(getMode(), { none: () => { const data = getData(); if (!data.pointerDown) return; if (!canDraw(event)) return; // If there's a movement threshold, do nothing, wait until the pointer moves first. if (movementThreshold) return; const region = adjustDrawingRegion(event, data.pointerDown); const start = { ...adjustDrawingPoint(event, data.pointerDown.head), id: uuid(), }; setMode({ type: "drawing", region }); setData({ ...data, drawnPoints: [start] }); addDrawnPoint(event, start); event.preventDefault(); }, }); }, onPointerMove(event) { modeSwitch(getMode(), { none: () => { const data = getData(); if (!data.pointerDown) return; if (!canDraw(event)) return; if ( isMoving( data.pointerDown.anchor, data.pointerDown.head, data.zoom, movementThreshold ) ) { const region = adjustDrawingRegion(event, data.pointerDown); const start = { ...adjustDrawingPoint(event, data.pointerDown.anchor), id: uuid(), }; const end = { ...adjustDrawingPoint(event, data.pointerDown.head), id: uuid(), }; setMode({ type: "drawing", region }); setData({ ...data, drawnPoints: [start, end] }); addDrawnPoint(event, start); addDrawnPoint(event, end); event.preventDefault(); } }, drawing: () => { const data = getData(); if (!data.pointerDown) return; const region = adjustDrawingRegion(event, data.pointerDown); const end = { ...adjustDrawingPoint(event, data.pointerDown.head), id: uuid(), }; setMode({ type: "drawing", region }); setData({ ...data, drawnPoints: [...data.drawnPoints, end] }); addDrawnPoint(event, end); event.preventDefault(); }, }); }, onPointerUp(event) { const mode = getMode(); if (mode.type !== "drawing") return; const data = getData(); if (!data.pointerDown) return; let newMode: DrawingMode | undefined; // If not a nearly-zero area, add the drawn element if ( isMoving( data.pointerDown.anchor, data.pointerDown.head, data.zoom, movementThreshold ) ) { const region = adjustDrawingRegion(event, data.pointerDown); const end = { ...adjustDrawingPoint(event, data.pointerDown.head), id: uuid(), }; addDrawnPoint(event, end); // this can be any mode, including ones that the drawing plugin doesn't handle newMode = addDrawnElement(event, region) as DrawingMode | undefined; } setMode(newMode ?? { type: "none" }); // Use current data in case adding the drawn element modifies it setData((data) => ({ ...data, drawnPoints: [] })); event.preventDefault(); }, }), }); }