diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index af66ebd9fe..25c09732dc 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -24,7 +24,6 @@ import { pointsEqual, lineSegmentIntersectionPoints, PRECISION, - doBoundsIntersect, } from "@excalidraw/math"; import type { LocalPoint, Radians } from "@excalidraw/math"; @@ -33,7 +32,11 @@ import type { AppState } from "@excalidraw/excalidraw/types"; import type { MapEntry, Mutable } from "@excalidraw/common/utility-types"; -import { getCenterForBounds, getElementBounds } from "./bounds"; +import { + doBoundsIntersect, + getCenterForBounds, + getElementBounds, +} from "./bounds"; import { intersectElementWithLineSegment } from "./collision"; import { distanceToElement } from "./distance"; import { diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index 260a9bd4ed..2c07631a7a 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -584,7 +584,7 @@ const solveQuadratic = ( return [s1, s2]; }; -const getCubicBezierCurveBound = ( +export const getCubicBezierCurveBound = ( p0: GlobalPoint, p1: GlobalPoint, p2: GlobalPoint, @@ -1230,6 +1230,20 @@ export const pointInsideBounds =

( ): boolean => p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; +export const doBoundsIntersect = ( + bounds1: Bounds | null, + bounds2: Bounds | null, +): boolean => { + if (bounds1 == null || bounds2 == null) { + return false; + } + + const [minX1, minY1, maxX1, maxY1] = bounds1; + const [minX2, minY2, maxX2, maxY2] = bounds2; + + return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; +}; + export const elementCenterPoint = ( element: ExcalidrawElement, elementsMap: ElementsMap, diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 5ae7753e63..cc15947edb 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -11,7 +11,6 @@ import { vectorFromPoint, vectorNormalize, vectorScale, - doBoundsIntersect, } from "@excalidraw/math"; import { @@ -19,15 +18,22 @@ import { ellipseSegmentInterceptPoints, } from "@excalidraw/math/ellipse"; -import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math"; +import type { + Curve, + GlobalPoint, + LineSegment, + Radians, +} from "@excalidraw/math"; import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; import { isPathALoop } from "./utils"; import { type Bounds, + doBoundsIntersect, elementCenterPoint, getCenterForBounds, + getCubicBezierCurveBound, getElementBounds, } from "./bounds"; import { @@ -255,13 +261,75 @@ export const intersectElementWithLineSegment = ( } }; +const curveIntersections = ( + curves: Curve[], + segment: LineSegment, + intersections: GlobalPoint[], + center: GlobalPoint, + angle: Radians, + onlyFirst = false, +) => { + for (const c of curves) { + // Optimize by doing a cheap bounding box check first + const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]); + const b2 = [ + Math.min(segment[0][0], segment[1][0]), + Math.min(segment[0][1], segment[1][1]), + Math.max(segment[0][0], segment[1][0]), + Math.max(segment[0][1], segment[1][1]), + ] as Bounds; + + if (!doBoundsIntersect(b1, b2)) { + continue; + } + + const hits = curveIntersectLineSegment(c, segment); + + if (hits.length > 0) { + for (const j of hits) { + intersections.push(pointRotateRads(j, center, angle)); + } + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; +}; + +const lineIntersections = ( + lines: LineSegment[], + segment: LineSegment, + intersections: GlobalPoint[], + center: GlobalPoint, + angle: Radians, + onlyFirst = false, +) => { + for (const l of lines) { + const intersection = lineSegmentIntersectionPoints(l, segment); + if (intersection) { + intersections.push(pointRotateRads(intersection, center, angle)); + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; +}; + const intersectLinearOrFreeDrawWithLineSegment = ( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, segment: LineSegment, onlyFirst = false, ): GlobalPoint[] => { + // NOTE: This is the only one which return the decomposed elements + // rotated! This is due to taking advantage of roughjs definitions. const [lines, curves] = deconstructLinearOrFreeDrawElement(element); - const intersections = []; + const intersections: GlobalPoint[] = []; for (const l of lines) { const intersection = lineSegmentIntersectionPoints(l, segment); @@ -275,6 +343,19 @@ const intersectLinearOrFreeDrawWithLineSegment = ( } for (const c of curves) { + // Optimize by doing a cheap bounding box check first + const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]); + const b2 = [ + Math.min(segment[0][0], segment[1][0]), + Math.min(segment[0][1], segment[1][1]), + Math.max(segment[0][0], segment[1][0]), + Math.max(segment[0][1], segment[1][1]), + ] as Bounds; + + if (!doBoundsIntersect(b1, b2)) { + continue; + } + const hits = curveIntersectLineSegment(c, segment); if (hits.length > 0) { @@ -292,7 +373,7 @@ const intersectLinearOrFreeDrawWithLineSegment = ( const intersectRectanguloidWithLineSegment = ( element: ExcalidrawRectanguloidElement, elementsMap: ElementsMap, - l: LineSegment, + segment: LineSegment, offset: number = 0, onlyFirst = false, ): GlobalPoint[] => { @@ -300,48 +381,43 @@ const intersectRectanguloidWithLineSegment = ( // To emulate a rotated rectangle we rotate the point in the inverse angle // instead. It's all the same distance-wise. const rotatedA = pointRotateRads( - l[0], + segment[0], center, -element.angle as Radians, ); const rotatedB = pointRotateRads( - l[1], + segment[1], center, -element.angle as Radians, ); + const rotatedIntersector = lineSegment(rotatedA, rotatedB); // Get the element's building components we can test against const [sides, corners] = deconstructRectanguloidElement(element, offset); const intersections: GlobalPoint[] = []; - for (const s of sides) { - const intersection = lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ); - if (intersection) { - intersections.push(pointRotateRads(intersection, center, element.angle)); + lineIntersections( + sides, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); - if (onlyFirst) { - return intersections; - } - } + if (onlyFirst && intersections.length > 0) { + return intersections; } - for (const t of corners) { - const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)); - - if (hits.length > 0) { - for (const j of hits) { - intersections.push(pointRotateRads(j, center, element.angle)); - } - - if (onlyFirst) { - return intersections; - } - } - } + curveIntersections( + corners, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); return intersections; }; @@ -366,38 +442,32 @@ const intersectDiamondWithLineSegment = ( // points. It's all the same distance-wise. const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); + const rotatedIntersector = lineSegment(rotatedA, rotatedB); const [sides, corners] = deconstructDiamondElement(element, offset); - const intersections: GlobalPoint[] = []; - for (const s of sides) { - const intersection = lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ); - if (intersection) { - intersections.push(pointRotateRads(intersection, center, element.angle)); + lineIntersections( + sides, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); - if (onlyFirst) { - return intersections; - } - } + if (onlyFirst && intersections.length > 0) { + return intersections; } - for (const t of corners) { - const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)); - - if (hits.length > 0) { - for (const j of hits) { - intersections.push(pointRotateRads(j, center, element.angle)); - } - - if (onlyFirst) { - return intersections; - } - } - } + curveIntersections( + corners, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); return intersections; }; diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index ca31639d5c..44b0fe79c6 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -90,6 +90,12 @@ const setElementShapesCacheEntry = ( shapes.set(offset, shape); }; +/** + * Returns the **rotated** components of freedraw, line or arrow elements. + * + * @param element The linear element to deconstruct + * @returns The rotated in components. + */ export function deconstructLinearOrFreeDrawElement( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, ): [LineSegment[], Curve[]] { @@ -171,11 +177,11 @@ export function deconstructLinearOrFreeDrawElement( /** * Get the building components of a rectanguloid element in the form of - * line segments and curves. + * line segments and curves **unrotated**. * * @param element Target rectanguloid element * @param offset Optional offset to expand the rectanguloid shape - * @returns Tuple of line segments (0) and curves (1) + * @returns Tuple of **unrotated** line segments (0) and curves (1) */ export function deconstructRectanguloidElement( element: ExcalidrawRectanguloidElement, @@ -310,12 +316,12 @@ export function deconstructRectanguloidElement( } /** - * Get the building components of a diamond element in the form of - * line segments and curves as a tuple, in this order. + * Get the **unrotated** 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) + * @returns Tuple of line **unrotated** segments (0) and curves (1) */ export function deconstructDiamondElement( element: ExcalidrawDiamondElement, diff --git a/packages/excalidraw/lasso/utils.ts b/packages/excalidraw/lasso/utils.ts index 8256f0069a..2cab64662b 100644 --- a/packages/excalidraw/lasso/utils.ts +++ b/packages/excalidraw/lasso/utils.ts @@ -4,12 +4,12 @@ import { polygonFromPoints, lineSegment, polygonIncludesPointNonZero, - doBoundsIntersect, } from "@excalidraw/math"; import { type Bounds, computeBoundTextPosition, + doBoundsIntersect, getBoundTextElement, getElementBounds, intersectElementWithLineSegment, diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 3323456b51..fa11abd460 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -1,9 +1,6 @@ -import { type Bounds } from "@excalidraw/element"; - import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point"; import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector"; import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants"; -import { doBoundsIntersect } from "./utils"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; @@ -105,19 +102,6 @@ export const bezierEquation = ( export function curveIntersectLineSegment< Point extends GlobalPoint | LocalPoint, >(c: Curve, l: LineSegment): Point[] { - // Optimize by doing a cheap bounding box check first - const b1 = curveBounds(c); - const b2 = [ - Math.min(l[0][0], l[1][0]), - Math.min(l[0][1], l[1][1]), - Math.max(l[0][0], l[1][0]), - Math.max(l[0][1], l[1][1]), - ] as Bounds; - - if (!doBoundsIntersect(b1, b2)) { - return []; - } - const line = (s: number) => pointFrom( l[0][0] + s * (l[1][0] - l[0][0]), @@ -295,15 +279,6 @@ export function curveTangent( ); } -function curveBounds( - c: Curve, -): Bounds { - const [P0, P1, P2, P3] = c; - const x = [P0[0], P1[0], P2[0], P3[0]]; - const y = [P0[1], P1[1], P2[1], P3[1]]; - return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)]; -} - export function curveCatmullRomQuadraticApproxPoints( points: GlobalPoint[], tension = 0.5, diff --git a/packages/math/src/rectangle.ts b/packages/math/src/rectangle.ts index 394b5c2f83..865e4afe53 100644 --- a/packages/math/src/rectangle.ts +++ b/packages/math/src/rectangle.ts @@ -10,6 +10,12 @@ export function rectangle

( return [topLeft, bottomRight] as Rectangle

; } +export function rectangleFromNumberSequence< + Point extends LocalPoint | GlobalPoint, +>(minX: number, minY: number, maxX: number, maxY: number) { + return rectangle(pointFrom(minX, minY), pointFrom(maxX, maxY)); +} + export function rectangleIntersectLineSegment< Point extends LocalPoint | GlobalPoint, >(r: Rectangle, l: LineSegment): Point[] { @@ -22,3 +28,12 @@ export function rectangleIntersectLineSegment< .map((s) => lineSegmentIntersectionPoints(l, s)) .filter((i): i is Point => !!i); } + +export function rectangleIntersectRectangle< + Point extends LocalPoint | GlobalPoint, +>(rectangle1: Rectangle, rectangle2: Rectangle): boolean { + const [[minX1, minY1], [maxX1, maxY1]] = rectangle1; + const [[minX2, minY2], [maxX2, maxY2]] = rectangle2; + + return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; +} diff --git a/packages/math/src/utils.ts b/packages/math/src/utils.ts index 912ddd1358..8807c275e4 100644 --- a/packages/math/src/utils.ts +++ b/packages/math/src/utils.ts @@ -1,5 +1,3 @@ -import { type Bounds } from "@excalidraw/element"; - export const PRECISION = 10e-5; export const clamp = (value: number, min: number, max: number) => { @@ -33,17 +31,3 @@ export const isFiniteNumber = (value: any): value is number => { export const isCloseTo = (a: number, b: number, precision = PRECISION) => Math.abs(a - b) < precision; - -export const doBoundsIntersect = ( - bounds1: Bounds | null, - bounds2: Bounds | null, -): boolean => { - if (bounds1 == null || bounds2 == null) { - return false; - } - - const [minX1, minY1, maxX1, maxY1] = bounds1; - const [minX2, minY2, maxX2, maxY2] = bounds2; - - return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; -};