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, ScreenPointProperties, } from "./contextProperties"; import { NoneMode } from "./PluginSystem"; import { PointerData } from "./pointerPlugin"; export type LineHandleCoordinates = { rect: Rect; start: IdentifiablePoint; end: IdentifiablePoint; }; type LineMode = | NoneMode | { type: "movingLinePoint"; region: Region; snapshot: StateSnapshot; id: string; }; export type LineData = { highlightedPointId?: string; pointHandleSize: number; zoom: number; } & Pick; export type LinePluginOptions = { pointHandleSize?: number; movementThreshold?: number; }; export interface LineContextProperties extends BaseContextProperties, LineData>, ScreenPointProperties { getLine: () => LineHandleCoordinates | undefined; getStateSnapshot: () => StateSnapshot; moveLinePoint: (params: { id: string; region: Region; snapshot: StateSnapshot; }) => void; canEditLine?: () => boolean; } function getLineHandles( line: LineHandleCoordinates, data: Pick ) { const transform = getAbsolutizeTransform(line.rect); return getPointHandles({ points: [line.start, line.end].map((point) => ({ ...transform.applyTo(point), id: point.id, })), data, }); } export function linePlugin(options: LinePluginOptions = {}) { const { pointHandleSize = DEFAULT_POINT_HANDLE_SIZE, movementThreshold = 0 } = options; return createCanvasPlugin({ name: "line", data: { highlightedPointId: undefined, pointHandleSize, zoom: 1, }, handlers: (context: LineContextProperties) => ({ onPointerDown(event) { if (context.getMode().type !== "none") return; if (context.canEditLine && !context.canEditLine()) return; const data = context.getData(); const pointerDown = data.pointerDown; if (!pointerDown) return; const line = context.getLine(); if (!line) return; const handles = getLineHandles(line, data); const handle = handles.find((candidate) => rectContainsPoint(candidate.rect, pointerDown.anchor) ); if (!handle) return; context.setMode({ type: "movingLinePoint", region: pointerDown, snapshot: context.getStateSnapshot(), id: handle.id, }); event.preventDefault(); }, onPointerMove(event) { const canEditLine = context.canEditLine?.() ?? true; if (!canEditLine) return; const data = context.getData(); const line = context.getLine(); if (!line) { if (data.highlightedPointId) { context.setData((current) => ({ ...current, highlightedPointId: undefined, })); } return; } const handles = getLineHandles(line, data); const mode = context.getMode(); if (mode.type === "none") { if (!data.pointerDown) { const pointer = context.getCanvasPoint(event.nativeEvent); const hoveredHandle = handles.find((handle) => rectContainsPoint(handle.rect, pointer) ); if (hoveredHandle?.id !== data.highlightedPointId) { context.setData((current) => ({ ...current, highlightedPointId: hoveredHandle?.id, })); } return; } return; } if (mode.type === "movingLinePoint" && data.pointerDown) { if ( isMoving( data.pointerDown.anchor, data.pointerDown.head, data.zoom, movementThreshold ) ) { context.moveLinePoint({ id: mode.id, region: data.pointerDown, snapshot: mode.snapshot, }); event.preventDefault(); } } }, // eslint-disable-next-line @shopify/prefer-early-return onPointerUp(event) { if (context.getMode().type === "movingLinePoint") { context.setMode({ type: "none" }); event.preventDefault(); } }, onPointerLeave() { if (context.getMode().type !== "none") return; const data = context.getData(); if (data.highlightedPointId) { context.setData((current) => ({ ...current, highlightedPointId: undefined, })); } }, onPointerCancel() { if (context.getMode().type !== "none") { context.setMode({ type: "none" }); } }, }), getCursor: ({ data, mode }) => { if ( (mode.type === "movingLinePoint" || mode.type === "none") && data.highlightedPointId ) { return "pointer"; } return undefined; }, }); }