import { Rect, rectContainsPoint, Region } from "@noya-app/noya-geometry"; import { getPointHandles } from "../utils/dragHandleUtils"; import { isMoving } from "../utils/isMoving"; import { DEFAULT_POINT_HANDLE_SIZE, getAbsolutizeTransform, IdentifiablePoint, } from "../utils/pointUtils"; import { createCanvasPlugin } from "./baseCanvas"; import { BaseContextProperties, PointHighlightableProperties, PointProperties, ScreenPointProperties, } from "./contextProperties"; import { NoneMode } from "./PluginSystem"; import { PointerData } from "./pointerPlugin"; type PointMode = | NoneMode | { type: "movingPoint"; region: Region; snapshot: StateSnapshot; id: string; }; export type PointData = { highlightedPointId?: string; pointHandleSize: number; zoom: number; } & Pick; type PointPluginOptions = { pointHandleSize?: number; movementThreshold?: number; pointHandleHitSize?: number; }; interface PointContextProperties extends BaseContextProperties, PointData>, ScreenPointProperties, PointProperties, PointHighlightableProperties { getStateSnapshot: () => StateSnapshot; getPoints: () => { points: IdentifiablePoint[]; rect: Rect } | undefined; movePoint: (params: { id: string; region: Region; snapshot: StateSnapshot; }) => void; } export function pointPlugin(options: PointPluginOptions = {}) { const { pointHandleSize = DEFAULT_POINT_HANDLE_SIZE, movementThreshold = 0, pointHandleHitSize, } = options; const handleHitSize = pointHandleHitSize ?? pointHandleSize; return createCanvasPlugin({ name: "point", data: { highlightedPointId: undefined, pointHandleSize, zoom: 1, }, handlers: (context: PointContextProperties) => ({ onPointerDown(event) { if (context.getMode().type !== "none") return; const data = context.getData(); const pointerDown = data.pointerDown; if (!pointerDown) return; const points = context.getPoints(); if (!points) return; const pointHandles = getPointHandles({ points: points.points.map((point) => getAbsolutizeTransform(points.rect).applyTo(point) ), data: { ...data, pointHandleSize: handleHitSize }, }); const pointHandle = pointHandles.find((handle) => rectContainsPoint(handle.rect, pointerDown.anchor) ); if (!pointHandle) return; context.setMode({ type: "movingPoint", region: pointerDown, id: pointHandle.id, snapshot: context.getStateSnapshot(), }); event.preventDefault(); }, onPointerMove(event) { const mode = context.getMode(); const data = context.getData(); const pointerDown = data.pointerDown; if (mode.type === "none") { if (!pointerDown) { const point = context.getCanvasPoint(event.nativeEvent); const points = context.filterPoints({ at: { x: point.x - handleHitSize / 2, y: point.y - handleHitSize / 2, width: handleHitSize, height: handleHitSize, }, predicate: context.isPointHighlightable, }); const firstPointId = points.length > 0 ? points[0].id : undefined; if (firstPointId !== data.highlightedPointId) { context.setData({ ...data, highlightedPointId: firstPointId }); } return; } return; } if (mode.type === "movingPoint" && pointerDown) { const shouldMove = isMoving( pointerDown.anchor, pointerDown.head, data.zoom, movementThreshold ); if (shouldMove) { context.movePoint({ id: mode.id, region: pointerDown, snapshot: mode.snapshot, }); event.preventDefault(); } } }, onPointerUp(event) { const mode = context.getMode(); if (mode.type === "movingPoint") { context.setMode({ type: "none" }); event.preventDefault(); } }, onPointerLeave() { const mode = context.getMode(); if (mode.type !== "none") return; const data = context.getData(); if (data.highlightedPointId) { context.setData({ ...data, highlightedPointId: undefined }); } }, }), }); }