diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index 2b88d563cf..eb56ea04ff 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -68,6 +68,7 @@ import { pointFromVector, vectorScale, vectorNormalize, + vectorRotate, } from "../../math"; import { distanceToBindableElement } from "./distance"; import { intersectElementWithLine } from "./collision"; @@ -1953,6 +1954,3 @@ export const normalizeFixedPoint = ( } return fixedPoint as any as T extends null ? null : FixedPoint; }; -function vectorRotate(arg0: any, arg1: any): import("../../math").Vector { - throw new Error("Function not implemented."); -} diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index 76f143c886..ab723da688 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -6,7 +6,7 @@ import type { ExcalidrawRectangleElement, ExcalidrawRectanguloidElement, } from "./types"; -import { getElementBounds } from "./bounds"; +import { getDiamondPoints, getElementBounds } from "./bounds"; import type { FrameNameBounds } from "../types"; import type { GeometricShape } from "../../utils/geometry/shape"; import { getPolygonShape } from "../../utils/geometry/shape"; @@ -20,7 +20,6 @@ import { } from "./typeChecks"; import { getBoundTextShape, getCornerRadius, isPathALoop } from "../shapes"; import type { - Arc, GlobalPoint, Line, LocalPoint, @@ -30,6 +29,8 @@ import type { import { arc, arcLineInterceptPoints, + curve, + curveIntersectLine, isPointWithinBounds, line, lineSegment, @@ -41,6 +42,12 @@ import { rectangle, } from "../../math"; import { ellipse, ellipseLineIntersectionPoints } from "../../math/ellipse"; +import { + debugClear, + debugDrawArc, + debugDrawCubicBezier, + debugDrawLine, +} from "../visualdebug"; export const shouldTestInside = (element: ExcalidrawElement) => { if (element.type === "arrow") { @@ -209,7 +216,7 @@ const intersectRectanguloidWithLine = ( element, ); - const sideIntersections: GlobalPoint[] = [ + const sides = [ lineSegment( pointFrom(r[0][0] + roundness, r[0][1]), pointFrom(r[1][0] - roundness, r[0][1]), @@ -226,25 +233,20 @@ const intersectRectanguloidWithLine = ( pointFrom(r[0][0], r[1][1] - roundness), pointFrom(r[0][0], r[0][1] + roundness), ), - ] - .map((s) => - lineSegmentIntersectionPoints(line(rotatedA, rotatedB), s), - ) - .filter((x) => x != null) - .map((j) => pointRotateRads(j!, center, element.angle)); - const cornerIntersections: GlobalPoint[] = + ]; + const corners = roundness > 0 ? [ arc( pointFrom(r[0][0] + roundness, r[0][1] + roundness), roundness, - ((3 / 4) * Math.PI) as Radians, - 0 as Radians, + Math.PI as Radians, + ((3 / 2) * Math.PI) as Radians, ), arc( pointFrom(r[1][0] - roundness, r[0][1] + roundness), roundness, - ((3 / 4) * Math.PI) as Radians, + ((3 / 2) * Math.PI) as Radians, 0 as Radians, ), arc( @@ -260,11 +262,24 @@ const intersectRectanguloidWithLine = ( Math.PI as Radians, ), ] - .flatMap((t) => arcLineInterceptPoints(t, line(rotatedA, rotatedB))) - .filter((i) => i != null) - .map((j) => pointRotateRads(j, center, element.angle)) : []; + debugClear(); + sides.forEach((s) => debugDrawLine(s, { color: "red", permanent: true })); + corners.forEach((s) => debugDrawArc(s, { color: "green", permanent: true })); + + const sideIntersections: GlobalPoint[] = sides + .map((s) => + lineSegmentIntersectionPoints(line(rotatedA, rotatedB), s), + ) + .filter((x) => x != null) + .map((j) => pointRotateRads(j!, center, element.angle)); + + const cornerIntersections: GlobalPoint[] = corners + .flatMap((t) => arcLineInterceptPoints(t, line(rotatedA, rotatedB))) + .filter((i) => i != null) + .map((j) => pointRotateRads(j, center, element.angle)); + return ( [...sideIntersections, ...cornerIntersections] // Remove duplicates @@ -287,105 +302,73 @@ const intersectDiamondWithLine = ( l: Line, offset: number = 0, ): GlobalPoint[] => { - const top = pointFrom( - element.x + element.width / 2, - element.y - offset, - ); - const right = pointFrom( - element.x + element.width + offset, - element.y + element.height / 2, - ); - const bottom = pointFrom( - element.x + element.width / 2, - element.y + element.height + offset, - ); - const left = pointFrom( - element.x - offset, - element.y + element.height / 2, - ); + const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = + getDiamondPoints(element); const center = pointFrom( - element.x + element.width / 2, - element.y + element.height / 2, - ); - const verticalRadius = getCornerRadius(Math.abs(top[0] - left[0]), element); - const horizontalRadius = getCornerRadius( - Math.abs(right[1] - top[1]), - element, + (topX + bottomX) / 2, + (topY + bottomY) / 2, ); + const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); + const horizontalRadius = getCornerRadius(Math.abs(rightY - topY), element); // Rotate the point to the inverse direction to simulate the rotated diamond // 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 [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), + ]; + + // Create the line segment parts of the diamond + // NOTE: Horizontal and vertical seems to be flipped here const topRight = lineSegment( - pointFrom(top[0] + verticalRadius, top[1] + horizontalRadius), - pointFrom(right[0] - verticalRadius, right[1] - horizontalRadius), + pointFrom(top[0] + horizontalRadius, top[1] + verticalRadius), + pointFrom(right[0] - horizontalRadius, right[1] - verticalRadius), ); const bottomRight = lineSegment( - pointFrom(bottom[0] + verticalRadius, bottom[1] - horizontalRadius), - pointFrom(right[0] - verticalRadius, right[1] + horizontalRadius), + pointFrom(bottom[0] + horizontalRadius, bottom[1] - verticalRadius), + pointFrom(right[0] - horizontalRadius, right[1] + verticalRadius), ); const bottomLeft = lineSegment( - pointFrom(bottom[0] - verticalRadius, bottom[1] - horizontalRadius), - pointFrom(left[0] + verticalRadius, left[1] + horizontalRadius), + pointFrom(bottom[0] - horizontalRadius, bottom[1] - verticalRadius), + pointFrom(left[0] + horizontalRadius, left[1] + verticalRadius), ); const topLeft = lineSegment( - pointFrom(top[0] - verticalRadius, top[1] + horizontalRadius), - pointFrom(left[0] + verticalRadius, left[1] - horizontalRadius), + pointFrom(top[0] - horizontalRadius, top[1] + verticalRadius), + pointFrom(left[0] + horizontalRadius, left[1] - verticalRadius), ); - const arcs: Arc[] = element.roundness + const curves = element.roundness ? [ - createDiamondArc( - topLeft[0], - topRight[0], - pointFrom( - top[0], - top[1] + Math.sqrt(2 * Math.pow(verticalRadius, 2)) - offset, - ), - verticalRadius, - ), // TOP - createDiamondArc( - topRight[1], - bottomRight[1], - pointFrom( - right[0] - Math.sqrt(2 * Math.pow(horizontalRadius, 2)) + offset, - right[1], - ), - horizontalRadius, - ), // RIGHT - createDiamondArc( - bottomRight[0], - bottomLeft[0], - pointFrom( - bottom[0], - bottom[1] - Math.sqrt(2 * Math.pow(verticalRadius, 2)) + offset, - ), - verticalRadius, - ), // BOTTOM - createDiamondArc( - bottomLeft[1], - topLeft[1], - pointFrom( - left[0] + Math.sqrt(2 * Math.pow(horizontalRadius, 2)) - offset, - left[1], - ), - horizontalRadius, - ), // LEFT + curve(topRight[1], right, right, bottomRight[1]), // RIGHT + curve(bottomRight[0], bottom, bottom, bottomLeft[0]), // BOTTOM + curve(bottomLeft[1], left, left, topLeft[1]), // LEFT + curve(topLeft[0], top, top, topRight[0]), // LEFT ] : []; + debugClear(); + [topRight, bottomRight, bottomLeft, topLeft].forEach((s) => { + debugDrawLine(s, { color: "red", permanent: true }); + }); + curves.forEach((s) => { + debugDrawCubicBezier(s, { color: "green", permanent: true }); + }); + const sides: GlobalPoint[] = [topRight, bottomRight, bottomLeft, topLeft] .map((s) => lineSegmentIntersectionPoints(line(rotatedA, rotatedB), s), ) - .filter((x) => x != null) + .filter((p): p is GlobalPoint => p != null) // Rotate back intersection points .map((p) => pointRotateRads(p!, center, element.angle)); - const corners = arcs - .flatMap((x) => arcLineInterceptPoints(x, line(rotatedA, rotatedB))) - .filter((x) => x != null) + const corners = curves + .flatMap((p) => curveIntersectLine(p, line(rotatedA, rotatedB))) + .filter((p) => p != null) // Rotate back intersection points .map((p) => pointRotateRads(p, center, element.angle)); diff --git a/packages/excalidraw/element/distance.ts b/packages/excalidraw/element/distance.ts index b31f50bf48..72b2cdaf49 100644 --- a/packages/excalidraw/element/distance.ts +++ b/packages/excalidraw/element/distance.ts @@ -2,6 +2,8 @@ import type { GlobalPoint, Radians } from "../../math"; import { arc, arcDistanceFromPoint, + curve, + curvePointDistance, distanceToLineSegment, lineSegment, pointFrom, @@ -10,7 +12,7 @@ import { } from "../../math"; import { ellipse, ellipseDistanceFromPoint } from "../../math/ellipse"; import { getCornerRadius } from "../shapes"; -import { createDiamondArc, createDiamondSide } from "./bounds"; +import { getDiamondPoints } from "./bounds"; import type { ExcalidrawBindableElement, ExcalidrawDiamondElement, @@ -147,53 +149,31 @@ export const distanceToDiamondElement = ( pointFrom(element.x + leftX, element.y + leftY), ]; - const topRight = createDiamondSide( - lineSegment(top, right), - verticalRadius, - horizontalRadius, + // Create the line segment parts of the diamond + // NOTE: Horizontal and vertical seems to be flipped here + const topRight = lineSegment( + pointFrom(top[0] + verticalRadius, top[1] + horizontalRadius), + pointFrom(right[0] + verticalRadius, right[1] + horizontalRadius), ); - const bottomRight = createDiamondSide( - lineSegment(bottom, right), - verticalRadius, - horizontalRadius, + const bottomRight = lineSegment( + pointFrom(bottom[0] + verticalRadius, bottom[1] + horizontalRadius), + pointFrom(right[0] + verticalRadius, right[1] + horizontalRadius), ); - const bottomLeft = createDiamondSide( - lineSegment(bottom, left), - verticalRadius, - horizontalRadius, + const bottomLeft = lineSegment( + pointFrom(bottom[0] + verticalRadius, bottom[1] + horizontalRadius), + pointFrom(left[0] + verticalRadius, left[1] + horizontalRadius), ); - const topLeft = createDiamondSide( - lineSegment(top, left), - verticalRadius, - horizontalRadius, + const topLeft = lineSegment( + pointFrom(top[0] + verticalRadius, top[1] + horizontalRadius), + pointFrom(left[0] + verticalRadius, left[1] + horizontalRadius), ); - const arcs = element.roundness + const curves = element.roundness ? [ - createDiamondArc( - topLeft[0], - topRight[0], - pointFrom(top[0], top[1] + verticalRadius), - verticalRadius, - ), // TOP - createDiamondArc( - topRight[1], - bottomRight[1], - pointFrom(right[0] - horizontalRadius, right[1]), - horizontalRadius, - ), // RIGHT - createDiamondArc( - bottomRight[0], - bottomLeft[0], - pointFrom(bottom[0], bottom[1] - verticalRadius), - verticalRadius, - ), // BOTTOM - createDiamondArc( - bottomLeft[1], - topLeft[1], - pointFrom(right[0] + horizontalRadius, right[1]), - horizontalRadius, - ), // LEFT + curve(topRight[1], right, right, bottomRight[1]), // RIGHT + curve(bottomRight[0], bottom, bottom, bottomLeft[0]), // BOTTOM + curve(bottomLeft[1], left, left, topLeft[1]), // LEFT + curve(topLeft[0], top, top, topRight[0]), // LEFT ] : []; @@ -202,7 +182,7 @@ export const distanceToDiamondElement = ( ...[topRight, bottomRight, bottomLeft, topLeft].map((s) => distanceToLineSegment(rotatedPoint, s), ), - ...arcs.map((a) => arcDistanceFromPoint(a, rotatedPoint)), + ...curves.map((a) => curvePointDistance(a, rotatedPoint)), ], ); }; diff --git a/packages/excalidraw/element/elbowArrow.ts b/packages/excalidraw/element/elbowArrow.ts index d3154cae91..10ca3f48b9 100644 --- a/packages/excalidraw/element/elbowArrow.ts +++ b/packages/excalidraw/element/elbowArrow.ts @@ -19,7 +19,6 @@ import { invariant, isAnyTrue, toBrandedType, tupleToCoors } from "../utils"; import type { AppState } from "../types"; import { bindPointToSnapToElementOutline, - distanceToBindableElement, avoidRectangularCorner, FIXED_BINDING_DISTANCE, getHeadingForElbowArrowSnap, @@ -55,6 +54,7 @@ import type { FixedPointBinding, FixedSegment, } from "./types"; +import { distanceToBindableElement } from "./distance"; type GridAddress = [number, number] & { _brand: "gridaddress" }; @@ -2164,7 +2164,7 @@ const getGlobalPoint = ( // NOTE: Resize scales the binding position point too, so we need to update it return Math.abs( - distanceToBindableElement(boundElement, fixedGlobalPoint, elementsMap) - + distanceToBindableElement(boundElement, fixedGlobalPoint) - FIXED_BINDING_DISTANCE, ) > 0.01 ? getSnapPoint(initialPoint, otherPoint, boundElement, elementsMap) @@ -2201,9 +2201,12 @@ const getBindPointHeading = ( hoveredElement && aabbForElement( hoveredElement, - Array(4).fill( - distanceToBindableElement(hoveredElement, p, elementsMap), - ) as [number, number, number, number], + Array(4).fill(distanceToBindableElement(hoveredElement, p)) as [ + number, + number, + number, + number, + ], ), elementsMap, origPoint, diff --git a/packages/math/curve.ts b/packages/math/curve.ts index 5350118009..72048c16c1 100644 --- a/packages/math/curve.ts +++ b/packages/math/curve.ts @@ -78,7 +78,7 @@ export function curveIntersectLine( return x; }) - .filter((x) => x !== null); + .filter((x): x is Point => x !== null); } /* @@ -244,7 +244,7 @@ export default function isCurve

( ): v is Curve

{ return ( Array.isArray(v) && - v.length !== 4 && + v.length === 4 && isPoint(v[0]) && isPoint(v[1]) && isPoint(v[2]) && diff --git a/packages/math/vector.ts b/packages/math/vector.ts index d7d51b14e9..1882575b72 100644 --- a/packages/math/vector.ts +++ b/packages/math/vector.ts @@ -1,4 +1,4 @@ -import type { GlobalPoint, LocalPoint, Vector } from "./types"; +import type { GlobalPoint, LocalPoint, Radians, Vector } from "./types"; /** * Create a vector from the x and y coordiante elements. @@ -140,6 +140,19 @@ export const vectorNormalize = (v: Vector): Vector => { return vector(v[0] / m, v[1] / m); }; +/** + * Rotate a vector by the given radians + * @param v Target vector + * @param a Angle to rotate in radians + * @returns The rotated vector + */ +export const vectorRotate = (v: Vector, a: Radians): Vector => { + const cos = Math.cos(a); + const sin = Math.sin(a); + + return vector(v[0] * cos - v[1] * sin, v[0] * sin + v[1] * cos); +}; + /** * Project the first vector onto the second vector */