import { curve, curveCatmullRomCubicApproxPoints, curveOffsetPoints, lineSegment, pointFrom, pointFromArray, rectangle, type GlobalPoint, } from "@excalidraw/math"; import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math"; import { getCornerRadius } from "./shapes"; import { getDiamondPoints } from "./bounds"; import { generateLinearCollisionShape } from "./Shape"; import type { ExcalidrawDiamondElement, ExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawLinearElement, ExcalidrawRectanguloidElement, } from "./types"; type ElementShape = [LineSegment[], Curve[]]; class ElementShapeCache { private static cache = new WeakMap< ExcalidrawElement, { version: ExcalidrawElement["version"]; shapes: Map } >(); public static get = ( element: T, offset: number, ): ElementShape | undefined => { const record = ElementShapeCache.cache.get(element); if (!record) { return undefined; } const { version, shapes } = record; if (version !== element.version) { ElementShapeCache.cache.delete(element); return undefined; } return shapes.get(offset); }; public static set = ( element: T, shape: ElementShape, offset: number, ) => { const record = ElementShapeCache.cache.get(element); if (!record) { ElementShapeCache.cache.set(element, { version: element.version, shapes: new Map([[offset, shape]]), }); return; } const { version, shapes } = record; if (version !== element.version) { ElementShapeCache.cache.set(element, { version: element.version, shapes: new Map([[offset, shape]]), }); return; } shapes.set(offset, shape); }; } export function deconstructLinearOrFreeDrawElement( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, ): [LineSegment[], Curve[]] { const cachedShape = ElementShapeCache.get(element, 0); if (cachedShape) { return cachedShape; } const ops = generateLinearCollisionShape(element) as { op: string; data: number[]; }[]; const lines = []; const curves = []; for (let idx = 0; idx < ops.length; idx += 1) { const op = ops[idx]; const prevPoint = ops[idx - 1] && pointFromArray(ops[idx - 1].data.slice(-2)); switch (op.op) { case "move": continue; case "lineTo": if (!prevPoint) { throw new Error("prevPoint is undefined"); } lines.push( lineSegment( pointFrom( element.x + prevPoint[0], element.y + prevPoint[1], ), pointFrom( element.x + op.data[0], element.y + op.data[1], ), ), ); continue; case "bcurveTo": if (!prevPoint) { throw new Error("prevPoint is undefined"); } curves.push( curve( pointFrom( element.x + prevPoint[0], element.y + prevPoint[1], ), pointFrom( element.x + op.data[0], element.y + op.data[1], ), pointFrom( element.x + op.data[2], element.y + op.data[3], ), pointFrom( element.x + op.data[4], element.y + op.data[5], ), ), ); continue; default: { console.error("Unknown op type", op.op); } } } const shape = [lines, curves] as ElementShape; ElementShapeCache.set(element, shape, 0); return shape; } /** * Get the building components of a rectanguloid element in the form of * line segments and curves. * * @param element Target rectanguloid element * @param offset Optional offset to expand the rectanguloid shape * @returns Tuple of line segments (0) and curves (1) */ export function deconstructRectanguloidElement( element: ExcalidrawRectanguloidElement, offset: number = 0, ): [LineSegment[], Curve[]] { const cachedShape = ElementShapeCache.get(element, offset); if (cachedShape) { return cachedShape; } let radius = getCornerRadius( Math.min(element.width, element.height), element, ); if (radius === 0) { radius = 0.01; } const r = rectangle( pointFrom(element.x, element.y), pointFrom(element.x + element.width, element.y + element.height), ); const top = lineSegment( pointFrom(r[0][0] + radius, r[0][1]), pointFrom(r[1][0] - radius, r[0][1]), ); const right = lineSegment( pointFrom(r[1][0], r[0][1] + radius), pointFrom(r[1][0], r[1][1] - radius), ); const bottom = lineSegment( pointFrom(r[0][0] + radius, r[1][1]), pointFrom(r[1][0] - radius, r[1][1]), ); const left = lineSegment( pointFrom(r[0][0], r[1][1] - radius), pointFrom(r[0][0], r[0][1] + radius), ); const baseCorners = [ curve( left[1], pointFrom( left[1][0] + (2 / 3) * (r[0][0] - left[1][0]), left[1][1] + (2 / 3) * (r[0][1] - left[1][1]), ), pointFrom( top[0][0] + (2 / 3) * (r[0][0] - top[0][0]), top[0][1] + (2 / 3) * (r[0][1] - top[0][1]), ), top[0], ), // TOP LEFT curve( top[1], pointFrom( top[1][0] + (2 / 3) * (r[1][0] - top[1][0]), top[1][1] + (2 / 3) * (r[0][1] - top[1][1]), ), pointFrom( right[0][0] + (2 / 3) * (r[1][0] - right[0][0]), right[0][1] + (2 / 3) * (r[0][1] - right[0][1]), ), right[0], ), // TOP RIGHT curve( right[1], pointFrom( right[1][0] + (2 / 3) * (r[1][0] - right[1][0]), right[1][1] + (2 / 3) * (r[1][1] - right[1][1]), ), pointFrom( bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]), bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]), ), bottom[1], ), // BOTTOM RIGHT curve( bottom[0], pointFrom( bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]), bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]), ), pointFrom( left[0][0] + (2 / 3) * (r[0][0] - left[0][0]), left[0][1] + (2 / 3) * (r[1][1] - left[0][1]), ), left[0], ), // BOTTOM LEFT ]; const corners = offset > 0 ? baseCorners.map( (corner) => curveCatmullRomCubicApproxPoints( curveOffsetPoints(corner, offset), )!, ) : [ [baseCorners[0]], [baseCorners[1]], [baseCorners[2]], [baseCorners[3]], ]; const sides = [ lineSegment( corners[0][corners[0].length - 1][3], corners[1][0][0], ), lineSegment( corners[1][corners[1].length - 1][3], corners[2][0][0], ), lineSegment( corners[2][corners[2].length - 1][3], corners[3][0][0], ), lineSegment( corners[3][corners[3].length - 1][3], corners[0][0][0], ), ]; const shape = [sides, corners.flat()] as ElementShape; ElementShapeCache.set(element, shape, offset); return shape; } /** * Get the building components of a diamond element in the form of * line segments and curves as a tuple, in this order. * * @param element The element to deconstruct * @param offset An optional offset * @returns Tuple of line segments (0) and curves (1) */ export function deconstructDiamondElement( element: ExcalidrawDiamondElement, offset: number = 0, ): [LineSegment[], Curve[]] { const cachedShape = ElementShapeCache.get(element, offset); if (cachedShape) { return cachedShape; } const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = getDiamondPoints(element); const verticalRadius = element.roundness ? getCornerRadius(Math.abs(topX - leftX), element) : (topX - leftX) * 0.01; const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) : (rightY - topY) * 0.01; const [top, right, bottom, left]: GlobalPoint[] = [ pointFrom(element.x + topX, element.y + topY), pointFrom(element.x + rightX, element.y + rightY), pointFrom(element.x + bottomX, element.y + bottomY), pointFrom(element.x + leftX, element.y + leftY), ]; const baseCorners = [ curve( pointFrom( right[0] - verticalRadius, right[1] - horizontalRadius, ), right, right, pointFrom( right[0] - verticalRadius, right[1] + horizontalRadius, ), ), // RIGHT curve( pointFrom( bottom[0] + verticalRadius, bottom[1] - horizontalRadius, ), bottom, bottom, pointFrom( bottom[0] - verticalRadius, bottom[1] - horizontalRadius, ), ), // BOTTOM curve( pointFrom( left[0] + verticalRadius, left[1] + horizontalRadius, ), left, left, pointFrom( left[0] + verticalRadius, left[1] - horizontalRadius, ), ), // LEFT curve( pointFrom( top[0] - verticalRadius, top[1] + horizontalRadius, ), top, top, pointFrom( top[0] + verticalRadius, top[1] + horizontalRadius, ), ), // TOP ]; const corners = offset > 0 ? baseCorners.map( (corner) => curveCatmullRomCubicApproxPoints( curveOffsetPoints(corner, offset), )!, ) : [ [baseCorners[0]], [baseCorners[1]], [baseCorners[2]], [baseCorners[3]], ]; const sides = [ lineSegment( corners[0][corners[0].length - 1][3], corners[1][0][0], ), lineSegment( corners[1][corners[1].length - 1][3], corners[2][0][0], ), lineSegment( corners[2][corners[2].length - 1][3], corners[3][0][0], ), lineSegment( corners[3][corners[3].length - 1][3], corners[0][0][0], ), ]; const shape = [sides, corners.flat()] as ElementShape; ElementShapeCache.set(element, shape, offset); return shape; }