diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index 19cde12d5..d424886db 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -6,6 +6,8 @@ import type { NonDeleted, ExcalidrawTextElementWithContainer, ElementsMap, + ExcalidrawRectanguloidElement, + ExcalidrawEllipseElement, } from "./types"; import rough from "roughjs/bin/rough"; import type { Point as RoughPoint } from "roughjs/bin/geometry"; @@ -25,6 +27,7 @@ import { LinearElementEditor } from "./linearElementEditor"; import { ShapeCache } from "../scene/ShapeCache"; import { arrayToMap, invariant } from "../utils"; import type { + Curve, Degrees, GlobalPoint, LineSegment, @@ -40,6 +43,12 @@ import { pointRotateRads, } from "../../math"; import type { Mutable } from "../utility-types"; +import { getElementShape } from "../shapes"; +import { pointsOnBezierCurves } from "points-on-curve"; +import { + deconstructDiamondElement, + deconstructRectanguloidElement, +} from "./utils"; export type RectangleBox = { x: number; @@ -246,50 +255,69 @@ export const getElementAbsoluteCoords = ( * that can be used for visual collision detection (useful for frames) * as opposed to bounding box collision detection */ +/** + * Given an element, return the line segments that make up the element. + * + * Uses helpers from /math + */ export const getElementLineSegments = ( element: ExcalidrawElement, elementsMap: ElementsMap, ): LineSegment[] => { + const shape = getElementShape(element, elementsMap); const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( element, elementsMap, ); + const center = pointFrom(cx, cy); - const center: GlobalPoint = pointFrom(cx, cy); - - if (isLinearElement(element) || isFreeDrawElement(element)) { - const segments: LineSegment[] = []; - + if (shape.type === "polycurve") { + const curves = shape.data; + const points = curves + .map((curve) => pointsOnBezierCurves(curve, 10)) + .flat(); let i = 0; - - while (i < element.points.length - 1) { + const segments: LineSegment[] = []; + while (i < points.length - 1) { segments.push( lineSegment( - pointRotateRads( - pointFrom( - element.points[i][0] + element.x, - element.points[i][1] + element.y, - ), - center, - element.angle, - ), - pointRotateRads( - pointFrom( - element.points[i + 1][0] + element.x, - element.points[i + 1][1] + element.y, - ), - center, - element.angle, - ), + pointFrom(points[i][0], points[i][1]), + pointFrom(points[i + 1][0], points[i + 1][1]), ), ); i++; } return segments; + } else if (shape.type === "polyline") { + return shape.data as LineSegment[]; + } else if (_isRectanguloidElement(element)) { + const [sides, corners] = deconstructRectanguloidElement(element); + const cornerSegments: LineSegment[] = corners + .map((corner) => getSegmentsOnCurve(corner, center, element.angle)) + .flat(); + const rotatedSides = getRotatedSides(sides, center, element.angle); + return [...rotatedSides, ...cornerSegments]; + } else if (element.type === "diamond") { + const [sides, corners] = deconstructDiamondElement(element); + const cornerSegments = corners + .map((corner) => getSegmentsOnCurve(corner, center, element.angle)) + .flat(); + const rotatedSides = getRotatedSides(sides, center, element.angle); + + return [...rotatedSides, ...cornerSegments]; + } else if (shape.type === "polygon") { + const points = shape.data as GlobalPoint[]; + const segments: LineSegment[] = []; + for (let i = 0; i < points.length - 1; i++) { + segments.push(lineSegment(points[i], points[i + 1])); + } + return segments; + } else if (shape.type === "ellipse") { + return getSegmentsOnEllipse(element as ExcalidrawEllipseElement); } - const [nw, ne, sw, se, n, s, w, e] = ( + const [nw, ne, sw, se, , , w, e] = ( [ [x1, y1], [x2, y1], @@ -302,28 +330,6 @@ export const getElementLineSegments = ( ] as GlobalPoint[] ).map((point) => pointRotateRads(point, center, element.angle)); - if (element.type === "diamond") { - return [ - lineSegment(n, w), - lineSegment(n, e), - lineSegment(s, w), - lineSegment(s, e), - ]; - } - - if (element.type === "ellipse") { - return [ - lineSegment(n, w), - lineSegment(n, e), - lineSegment(s, w), - lineSegment(s, e), - lineSegment(n, w), - lineSegment(n, e), - lineSegment(s, w), - lineSegment(s, e), - ]; - } - return [ lineSegment(nw, ne), lineSegment(sw, se), @@ -336,6 +342,94 @@ export const getElementLineSegments = ( ]; }; +const _isRectanguloidElement = ( + element: ExcalidrawElement, +): element is ExcalidrawRectanguloidElement => { + return ( + element != null && + (element.type === "rectangle" || + element.type === "image" || + element.type === "iframe" || + element.type === "embeddable" || + element.type === "frame" || + element.type === "magicframe" || + (element.type === "text" && !element.containerId)) + ); +}; + +const getRotatedSides = ( + sides: LineSegment[], + center: GlobalPoint, + angle: Radians, +) => { + return sides.map((side) => { + return lineSegment( + pointRotateRads(side[0], center, angle), + pointRotateRads(side[1], center, angle), + ); + }); +}; + +const getSegmentsOnCurve = ( + curve: Curve, + center: GlobalPoint, + angle: Radians, +): LineSegment[] => { + const points = pointsOnBezierCurves(curve, 10); + let i = 0; + const segments: LineSegment[] = []; + while (i < points.length - 1) { + segments.push( + lineSegment( + pointRotateRads( + pointFrom(points[i][0], points[i][1]), + center, + angle, + ), + pointRotateRads( + pointFrom(points[i + 1][0], points[i + 1][1]), + center, + angle, + ), + ), + ); + i++; + } + + return segments; +}; + +const getSegmentsOnEllipse = ( + ellipse: ExcalidrawEllipseElement, +): LineSegment[] => { + const center = pointFrom( + ellipse.x + ellipse.width / 2, + ellipse.y + ellipse.height / 2, + ); + + const a = ellipse.width / 2; + const b = ellipse.height / 2; + + const segments: LineSegment[] = []; + const points: GlobalPoint[] = []; + const n = 90; + const deltaT = (Math.PI * 2) / n; + + for (let i = 0; i < n; i++) { + const t = i * deltaT; + const x = center[0] + a * Math.cos(t); + const y = center[1] + b * Math.sin(t); + points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle)); + } + + for (let i = 0; i < points.length - 1; i++) { + segments.push(lineSegment(points[i], points[i + 1])); + } + + segments.push(lineSegment(points[points.length - 1], points[0])); + return segments; +}; + /** * Scene -> Scene coords, but in x1,x2,y1,y2 format. * diff --git a/packages/excalidraw/lasso/index.ts b/packages/excalidraw/lasso/index.ts index c5fffe14a..9528212bf 100644 --- a/packages/excalidraw/lasso/index.ts +++ b/packages/excalidraw/lasso/index.ts @@ -1,36 +1,19 @@ -import { pointsOnBezierCurves } from "points-on-curve"; -import { - type Curve, - type GlobalPoint, - type LineSegment, - type Radians, - lineSegment, - pointFrom, - pointRotateRads, -} from "../../math"; +import { type GlobalPoint, type LineSegment, pointFrom } from "../../math"; import { AnimatedTrail } from "../animated-trail"; import { type AnimationFrameHandler } from "../animation-frame-handler"; import type App from "../components/App"; +import { getElementLineSegments } from "../element/bounds"; import { LinearElementEditor } from "../element/linearElementEditor"; import { isFrameLikeElement, isLinearElement } from "../element/typeChecks"; import type { - ElementsMap, ExcalidrawElement, - ExcalidrawEllipseElement, ExcalidrawLinearElement, - ExcalidrawRectanguloidElement, NonDeleted, } from "../element/types"; import { getFrameChildren } from "../frame"; import { selectGroupsForSelectedElements } from "../groups"; -import { getElementShape } from "../shapes"; import { arrayToMap, easeOut } from "../utils"; -import type { LassoWorkerInput, LassoWorkerOutput } from "./worker"; -import { - deconstructDiamondElement, - deconstructRectanguloidElement, -} from "../element/utils"; -import { getElementAbsoluteCoords } from "../element"; +import type { LassoWorkerInput, LassoWorkerOutput } from "./types"; export class LassoTrail extends AnimatedTrail { private intersectedElements: Set = new Set(); @@ -187,178 +170,3 @@ export class LassoTrail extends AnimatedTrail { this.worker?.terminate(); } } - -/** - * Given an element, return the line segments that make up the element. - * - * Uses helpers from /math - */ -const getElementLineSegments = ( - element: ExcalidrawElement, - elementsMap: ElementsMap, -): LineSegment[] => { - const shape = getElementShape(element, elementsMap); - const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( - element, - elementsMap, - ); - const center = pointFrom(cx, cy); - - if (shape.type === "polycurve") { - const curves = shape.data; - const points = curves - .map((curve) => pointsOnBezierCurves(curve, 10)) - .flat(); - let i = 0; - const segments: LineSegment[] = []; - while (i < points.length - 1) { - segments.push( - lineSegment( - pointFrom(points[i][0], points[i][1]), - pointFrom(points[i + 1][0], points[i + 1][1]), - ), - ); - i++; - } - - return segments; - } else if (shape.type === "polyline") { - return shape.data as LineSegment[]; - } else if (isRectanguloidElement(element)) { - const [sides, corners] = deconstructRectanguloidElement(element); - const cornerSegments: LineSegment[] = corners - .map((corner) => getSegmentsOnCurve(corner, center, element.angle)) - .flat(); - const rotatedSides = getRotatedSides(sides, center, element.angle); - return [...rotatedSides, ...cornerSegments]; - } else if (element.type === "diamond") { - const [sides, corners] = deconstructDiamondElement(element); - const cornerSegments = corners - .map((corner) => getSegmentsOnCurve(corner, center, element.angle)) - .flat(); - const rotatedSides = getRotatedSides(sides, center, element.angle); - - return [...rotatedSides, ...cornerSegments]; - } else if (shape.type === "polygon") { - const points = shape.data as GlobalPoint[]; - const segments: LineSegment[] = []; - for (let i = 0; i < points.length - 1; i++) { - segments.push(lineSegment(points[i], points[i + 1])); - } - return segments; - } else if (shape.type === "ellipse") { - return getSegmentsOnEllipse(element as ExcalidrawEllipseElement); - } - - const [nw, ne, sw, se, , , w, e] = ( - [ - [x1, y1], - [x2, y1], - [x1, y2], - [x2, y2], - [cx, y1], - [cx, y2], - [x1, cy], - [x2, cy], - ] as GlobalPoint[] - ).map((point) => pointRotateRads(point, center, element.angle)); - - return [ - lineSegment(nw, ne), - lineSegment(sw, se), - lineSegment(nw, sw), - lineSegment(ne, se), - lineSegment(nw, e), - lineSegment(sw, e), - lineSegment(ne, w), - lineSegment(se, w), - ]; -}; - -const isRectanguloidElement = ( - element: ExcalidrawElement, -): element is ExcalidrawRectanguloidElement => { - return ( - element != null && - (element.type === "rectangle" || - element.type === "image" || - element.type === "iframe" || - element.type === "embeddable" || - element.type === "frame" || - element.type === "magicframe" || - (element.type === "text" && !element.containerId)) - ); -}; - -const getRotatedSides = ( - sides: LineSegment[], - center: GlobalPoint, - angle: Radians, -) => { - return sides.map((side) => { - return lineSegment( - pointRotateRads(side[0], center, angle), - pointRotateRads(side[1], center, angle), - ); - }); -}; - -const getSegmentsOnCurve = ( - curve: Curve, - center: GlobalPoint, - angle: Radians, -): LineSegment[] => { - const points = pointsOnBezierCurves(curve, 10); - let i = 0; - const segments: LineSegment[] = []; - while (i < points.length - 1) { - segments.push( - lineSegment( - pointRotateRads( - pointFrom(points[i][0], points[i][1]), - center, - angle, - ), - pointRotateRads( - pointFrom(points[i + 1][0], points[i + 1][1]), - center, - angle, - ), - ), - ); - i++; - } - - return segments; -}; - -const getSegmentsOnEllipse = ( - ellipse: ExcalidrawEllipseElement, -): LineSegment[] => { - const center = pointFrom( - ellipse.x + ellipse.width / 2, - ellipse.y + ellipse.height / 2, - ); - - const a = ellipse.width / 2; - const b = ellipse.height / 2; - - const segments: LineSegment[] = []; - const points: GlobalPoint[] = []; - const n = 90; - const deltaT = (Math.PI * 2) / n; - - for (let i = 0; i < n; i++) { - const t = i * deltaT; - const x = center[0] + a * Math.cos(t); - const y = center[1] + b * Math.sin(t); - points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle)); - } - - for (let i = 0; i < points.length - 1; i++) { - segments.push(lineSegment(points[i], points[i + 1])); - } - - segments.push(lineSegment(points[points.length - 1], points[0])); - return segments; -}; diff --git a/packages/excalidraw/lasso/types.ts b/packages/excalidraw/lasso/types.ts new file mode 100644 index 000000000..c8c4bdccc --- /dev/null +++ b/packages/excalidraw/lasso/types.ts @@ -0,0 +1,17 @@ +import type { GlobalPoint, LineSegment } from "../../math"; +import type { ExcalidrawElement } from "../element/types"; + +export type ElementsSegmentsMap = Map[]>; + +export type LassoWorkerInput = { + lassoPath: GlobalPoint[]; + elements: readonly ExcalidrawElement[]; + elementsSegments: ElementsSegmentsMap; + intersectedElements: Set; + enclosedElements: Set; + simplifyDistance: number; +}; + +export type LassoWorkerOutput = { + selectedElementIds: string[]; +}; diff --git a/packages/excalidraw/lasso/worker.ts b/packages/excalidraw/lasso/worker.ts index 74290a9a9..24d42915f 100644 --- a/packages/excalidraw/lasso/worker.ts +++ b/packages/excalidraw/lasso/worker.ts @@ -3,6 +3,11 @@ import { polygonFromPoints, polygonIncludesPoint } from "../../math/polygon"; import type { ExcalidrawElement } from "../element/types"; import { lineSegment, lineSegmentIntersectionPoints } from "../../math/segment"; import { simplify } from "points-on-curve"; +import type { + ElementsSegmentsMap, + LassoWorkerInput, + LassoWorkerOutput, +} from "./types"; // variables to track processing state and latest input data // for "backpressure" purposes @@ -71,21 +76,6 @@ const processInputData = () => { } }; -type ElementsSegments = Map[]>; - -export type LassoWorkerInput = { - lassoPath: GlobalPoint[]; - elements: readonly ExcalidrawElement[]; - elementsSegments: ElementsSegments; - intersectedElements: Set; - enclosedElements: Set; - simplifyDistance: number; -}; - -export type LassoWorkerOutput = { - selectedElementIds: string[]; -}; - export const updateSelection = (input: LassoWorkerInput): LassoWorkerOutput => { const { lassoPath, @@ -96,7 +86,7 @@ export const updateSelection = (input: LassoWorkerInput): LassoWorkerOutput => { simplifyDistance, } = input; // simplify the path to reduce the number of points - let path = simplify(lassoPath, simplifyDistance) as GlobalPoint[]; + const path = simplify(lassoPath, simplifyDistance) as GlobalPoint[]; // close the path to form a polygon for enclosure check const closedPath = polygonFromPoints(path); // as the path might not enclose a shape anymore, clear before checking @@ -132,7 +122,7 @@ export const updateSelection = (input: LassoWorkerInput): LassoWorkerOutput => { const enclosureTest = ( lassoPath: GlobalPoint[], element: ExcalidrawElement, - elementsSegments: ElementsSegments, + elementsSegments: ElementsSegmentsMap, ): boolean => { const lassoPolygon = polygonFromPoints(lassoPath); const segments = elementsSegments.get(element.id); @@ -148,7 +138,7 @@ const enclosureTest = ( const intersectionTest = ( lassoPath: GlobalPoint[], element: ExcalidrawElement, - elementsSegments: ElementsSegments, + elementsSegments: ElementsSegmentsMap, ): boolean => { const elementSegments = elementsSegments.get(element.id); if (!elementSegments) {