diff --git a/packages/element/src/Shape.ts b/packages/element/src/Shape.ts index 31ae391fa..2d0235bd3 100644 --- a/packages/element/src/Shape.ts +++ b/packages/element/src/Shape.ts @@ -1,12 +1,6 @@ import { simplify } from "points-on-curve"; -import { - pointFrom, - pointDistance, - type LocalPoint, - curve, - pointFromArray, -} from "@excalidraw/math"; +import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math"; import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common"; import { RoughGenerator } from "roughjs/bin/generator"; @@ -36,6 +30,7 @@ import type { ExcalidrawSelectionElement, ExcalidrawLinearElement, Arrowhead, + ExcalidrawFreeDrawElement, } from "./types"; import type { Drawable, Options } from "roughjs/bin/core"; @@ -68,37 +63,6 @@ function adjustRoughness(element: ExcalidrawElement): number { return Math.min(roughness / (maxSize < 10 ? 3 : 2), 2.5); } -export const generateRoughOptionsForCollision = ( - element: ExcalidrawElement, -): Options => { - const options: Options = { - seed: element.seed, - disableMultiStroke: true, - roughness: 0, - preserveVertices: true, - }; - - switch (element.type) { - case "rectangle": - case "iframe": - case "embeddable": - case "diamond": - case "ellipse": { - if (element.type === "ellipse") { - options.curveFitting = 1; - } - return options; - } - case "line": - case "freedraw": - case "arrow": - return options; - default: { - throw new Error(`Unimplemented type ${element.type}`); - } - } -}; - export const generateRoughOptions = ( element: ExcalidrawElement, continuousPath = false, @@ -341,50 +305,22 @@ const getArrowheadShapes = ( } }; -export const generateComponentsForCollision = (element: ExcalidrawElement) => { - const ops = generateRoughOpsForCollision(element) as { - op: string; - data: number[]; - }[]; - const components = []; - - 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 "bcurveTo": - if (!prevPoint) { - throw new Error("prevPoint is undefined"); - } - - components.push( - curve( - prevPoint, - pointFrom(op.data[0], op.data[1]), - pointFrom(op.data[2], op.data[3]), - pointFrom(op.data[4], op.data[5]), - ), - ); - continue; - default: { - console.error("Unknown op type", op.op); - } - } - } - - return components; -}; - -const generateRoughOpsForCollision = (element: ExcalidrawElement) => { +export const generateLinearCollisionShape = ( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, +) => { const generator = new RoughGenerator(); + const options: Options = { + seed: element.seed, + disableMultiStroke: true, + disableMultiStrokeFill: true, + roughness: 0, + preserveVertices: true, + }; + switch (element.type) { case "line": case "arrow": { let shape: any; - const options = generateRoughOptions(element); // points array can be empty in the beginning, so it is important to add // initial position to it @@ -393,42 +329,24 @@ const generateRoughOpsForCollision = (element: ExcalidrawElement) => { : [pointFrom(0, 0)]; if (isElbowArrow(element)) { - // NOTE (mtolmacs): Temporary fix for extremely big arrow shapes - if ( - !points.every( - (point) => Math.abs(point[0]) <= 1e6 && Math.abs(point[1]) <= 1e6, - ) - ) { - console.error( - `Elbow arrow with extreme point positions detected. Arrow not rendered.`, - element.id, - JSON.stringify(points), - ); - shape = []; - } else { - shape = generator.path( - generateElbowArrowShape(points, 16), - generateRoughOptionsForCollision(element), - ).sets[0].ops; - } - } else if (!element.roundness) { - // curve is always the first element - // this simplifies finding the curve for an element - if (options.fill) { - shape = generator.polygon(points as unknown as RoughPoint[], options) - .sets[0].ops; - } else { - shape = generator.linearPath( - points as unknown as RoughPoint[], - options, - ).sets[0].ops; - } - } else { - shape = generator.curve(points as unknown as RoughPoint[], options) + shape = generator.path(generateElbowArrowShape(points, 16), options) .sets[0].ops; + } else if (!element.roundness) { + shape = points.map((point, idx) => { + return idx === 0 + ? { op: "move", data: point } + : { + op: "lineTo", + data: [point[0], point[1]], + }; + }); + } else { + shape = generator + .curve(points as unknown as RoughPoint[], options) + .sets[0].ops.slice(0, element.points.length); } - return shape.slice(0, element.points.length); + return shape; } case "freedraw": { const simplifiedPoints = simplify( @@ -437,10 +355,7 @@ const generateRoughOpsForCollision = (element: ExcalidrawElement) => { ); return generator - .curve( - simplifiedPoints as [number, number][], - generateRoughOptionsForCollision(element), - ) + .curve(simplifiedPoints as [number, number][], options) .sets[0].ops.slice(0, element.points.length); } } diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index d8810c5a7..66df3ac3e 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -4,9 +4,9 @@ import { arrayToMap, } from "@excalidraw/common"; import { - curve, curveIntersectLineSegment, isCurve, + isLineSegment, isPointWithinBounds, lineSegment, lineSegmentIntersectionPoints, @@ -22,7 +22,12 @@ import { import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision"; -import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math"; +import type { + Curve, + GlobalPoint, + LineSegment, + Radians, +} from "@excalidraw/math"; import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; @@ -37,6 +42,7 @@ import { } from "./typeChecks"; import { deconstructDiamondElement, + deconstructLinearOrFreeDrawElement, deconstructRectanguloidElement, } from "./utils"; @@ -44,18 +50,16 @@ import { getBoundTextElement } from "./textElement"; import { LinearElementEditor } from "./linearElementEditor"; -import { generateComponentsForCollision } from "./Shape"; - import type { ElementsMap, ExcalidrawDiamondElement, ExcalidrawElement, ExcalidrawEllipseElement, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, ExcalidrawRectanguloidElement, } from "./types"; -import { debugDrawCubicBezier } from "@excalidraw/excalidraw/visualdebug"; - export const shouldTestInside = (element: ExcalidrawElement) => { if (element.type === "arrow") { return false; @@ -107,21 +111,6 @@ export const hitElementItself = ({ : isPointOnShape(point, element, threshold) : false; - element.type === "freedraw" && - generateComponentsForCollision(element).forEach((c) => { - if (isCurve(c)) { - debugDrawCubicBezier( - curve( - pointFrom(element.x + c[0][0], element.y + c[0][1]), - pointFrom(element.x + c[1][0], element.y + c[1][1]), - pointFrom(element.x + c[2][0], element.y + c[2][1]), - pointFrom(element.x + c[3][0], element.y + c[3][1]), - ), - { color: "red" }, - ); - } - }); - // hit test against a frame's name if (!hit && frameNameBound) { const x1 = frameNameBound.x - threshold; @@ -216,11 +205,41 @@ export const intersectElementWithLineSegment = ( case "line": case "freedraw": case "arrow": - return []; - //throw new Error(`Unimplemented element type '${element.type}'`); + return intersectLinearOrFreeDrawWithLineSegment(element, line); } }; +const intersectLinearOrFreeDrawWithLineSegment = ( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, + segment: LineSegment, +): GlobalPoint[] => { + const shapes = deconstructLinearOrFreeDrawElement(element); + const intersections: GlobalPoint[] = []; + + for (const shape of shapes) { + switch (true) { + case isCurve(shape): + intersections.push( + ...curveIntersectLineSegment(shape as Curve, segment), + ); + continue; + case isLineSegment(shape): + const point = lineSegmentIntersectionPoints( + segment, + shape as LineSegment, + ); + + if (point) { + intersections.push(point); + } + + continue; + } + } + + return intersections; +}; + const intersectRectanguloidWithLineSegment = ( element: ExcalidrawRectanguloidElement, l: LineSegment, diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index 57b1e4346..b270c5105 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -2,6 +2,7 @@ import { curve, lineSegment, pointFrom, + pointFromArray, pointFromVector, rectangle, vectorFromPoint, @@ -12,17 +13,90 @@ import { import { elementCenterPoint } from "@excalidraw/common"; -import type { Curve, LineSegment } 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, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, ExcalidrawRectanguloidElement, } from "./types"; +export function deconstructLinearOrFreeDrawElement( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, +): (Curve | LineSegment)[] { + const ops = generateLinearCollisionShape(element) as { + op: string; + data: number[]; + }[]; + const components = []; + + 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"); + } + + components.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"); + } + + components.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); + } + } + } + + return components; +} + /** * Get the building components of a rectanguloid element in the form of * line segments and curves.