diff --git a/packages/excalidraw/align.ts b/packages/excalidraw/align.ts index 98dcc4d7b3..3bf866d10f 100644 --- a/packages/excalidraw/align.ts +++ b/packages/excalidraw/align.ts @@ -42,8 +42,6 @@ const calculateTranslation = ( maxY: selectionBounds[3], midX: (selectionBounds[0] + selectionBounds[2]) / 2, midY: (selectionBounds[1] + selectionBounds[3]) / 2, - width: selectionBounds[2] - selectionBounds[0], - height: selectionBounds[3] - selectionBounds[1], }; const groupBounds = getCommonBounds(group); const groupBoundingBox = { @@ -53,8 +51,6 @@ const calculateTranslation = ( maxY: groupBounds[3], midX: (groupBounds[0] + groupBounds[2]) / 2, midY: (groupBounds[1] + groupBounds[3]) / 2, - width: groupBounds[2] - groupBounds[0], - height: groupBounds[3] - groupBounds[1], }; const [min, max]: ["minX" | "minY", "maxX" | "maxY"] = diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index 773364e2db..6ccda33313 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -16,7 +16,7 @@ import { isTextElement, } from "./typeChecks"; import { getBoundTextShape } from "../shapes"; -import type { GlobalPoint, LocalPoint, Polygon } from "../../math"; +import type { GlobalPoint, Polygon } from "../../math"; import { pathIsALoop, isPointWithinBounds, point } from "../../math"; import { LINE_CONFIRM_THRESHOLD } from "../constants"; @@ -48,10 +48,10 @@ export const shouldTestInside = (element: ExcalidrawElement) => { return isDraggableFromInside || isImageElement(element); }; -export type HitTestArgs = { - sceneCoords: Point; +export type HitTestArgs = { + sceneCoords: GlobalPoint; element: ExcalidrawElement; - shape: GeometricShape; + shape: GeometricShape; threshold?: number; frameNameBound?: FrameNameBounds | null; }; @@ -62,7 +62,7 @@ export const hitElementItself = ({ shape, threshold = 10, frameNameBound = null, -}: HitTestArgs) => { +}: HitTestArgs) => { let hit = shouldTestInside(element) ? // Since `inShape` tests STRICTLY againt the insides of a shape // we would need `onShape` as well to include the "borders" @@ -97,7 +97,7 @@ export const hitElementBoundingBox = ( }; export const hitElementBoundingBoxOnly = ( - hitArgs: HitTestArgs, + hitArgs: HitTestArgs, elementsMap: ElementsMap, ) => { return ( diff --git a/packages/math/arc.test.ts b/packages/math/arc.test.ts index 6243ec0bd7..456d1a1add 100644 --- a/packages/math/arc.test.ts +++ b/packages/math/arc.test.ts @@ -6,7 +6,7 @@ describe("point on arc", () => { it("should detect point on simple arc", () => { expect( isPointOnSymmetricArc( - arc(1, radians(-Math.PI / 4), radians(Math.PI / 4)), + arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), point(0.92291667, 0.385), ), ).toBe(true); @@ -14,7 +14,7 @@ describe("point on arc", () => { it("should not detect point outside of a simple arc", () => { expect( isPointOnSymmetricArc( - arc(1, radians(-Math.PI / 4), radians(Math.PI / 4)), + arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), point(-0.92291667, 0.385), ), ).toBe(false); @@ -22,7 +22,7 @@ describe("point on arc", () => { it("should not detect point with good angle but incorrect radius", () => { expect( isPointOnSymmetricArc( - arc(1, radians(-Math.PI / 4), radians(Math.PI / 4)), + arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), point(-0.5, 0.5), ), ).toBe(false); diff --git a/packages/math/arc.ts b/packages/math/arc.ts index 2c1935fa68..716db8b26f 100644 --- a/packages/math/arc.ts +++ b/packages/math/arc.ts @@ -1,5 +1,7 @@ -import { cartesian2Polar } from "./angle"; -import type { GenericPoint, Radians, SymmetricArc } from "./types"; +import { cartesian2Polar, radians } from "./angle"; +import { ellipse, interceptPointsOfLineAndEllipse } from "./ellipse"; +import { point } from "./point"; +import type { GenericPoint, LineSegment, Radians, SymmetricArc } from "./types"; import { PRECISION } from "./utils"; /** @@ -12,8 +14,13 @@ import { PRECISION } from "./utils"; * @param endAngle The end angle with 0 radians being the "northest" point * @returns The constructed symmetric arc */ -export function arc(radius: number, startAngle: Radians, endAngle: Radians) { - return { radius, startAngle, endAngle } as SymmetricArc; +export function arc( + center: Point, + radius: number, + startAngle: Radians, + endAngle: Radians, +) { + return { center, radius, startAngle, endAngle } as SymmetricArc; } /** @@ -21,10 +28,12 @@ export function arc(radius: number, startAngle: Radians, endAngle: Radians) { * is part of a circle contour centered on 0, 0. */ export function isPointOnSymmetricArc

( - { radius: arcRadius, startAngle, endAngle }: SymmetricArc, - point: P, + { center, radius: arcRadius, startAngle, endAngle }: SymmetricArc

, + p: P, ): boolean { - const [radius, angle] = cartesian2Polar(point); + const [radius, angle] = cartesian2Polar( + point(p[0] - center[0], p[1] - center[1]), + ); return startAngle < endAngle ? Math.abs(radius - arcRadius) < PRECISION && @@ -32,3 +41,27 @@ export function isPointOnSymmetricArc

( endAngle >= angle : startAngle <= angle || endAngle >= angle; } + +/** + * Returns the intersection point(s) of a line segment represented by a start + * point and end point and a symmetric arc. + */ +export function interceptOfSymmetricArcAndSegment( + a: Readonly>, + l: Readonly>, +): Point[] { + return interceptPointsOfLineAndEllipse( + ellipse(a.center, radians(0), a.radius, a.radius), + l, + ).filter((candidate) => { + const [candidateRadius, candidateAngle] = cartesian2Polar( + point(candidate[0] - a.center[0], candidate[1] - a.center[1]), + ); + + return a.startAngle < a.endAngle + ? Math.abs(a.radius - candidateRadius) < 0.0000001 && + a.startAngle <= candidateAngle && + a.endAngle >= candidateAngle + : a.startAngle <= candidateAngle || a.endAngle >= candidateAngle; + }); +} diff --git a/packages/math/ellipse.test.ts b/packages/math/ellipse.test.ts index 1306da27d5..d7336ecf8a 100644 --- a/packages/math/ellipse.test.ts +++ b/packages/math/ellipse.test.ts @@ -1,45 +1,54 @@ import { radians } from "./angle"; -import { pointInEllipse, pointOnEllipse } from "./ellipse"; +import { + ellipse, + interceptPointsOfLineAndEllipse, + pointInEllipse, + pointOnEllipse, +} from "./ellipse"; import { point } from "./point"; +import { lineSegment } from "./segment"; import type { Ellipse, GlobalPoint } from "./types"; describe("point and ellipse", () => { - const ellipse: Ellipse = { - center: point(0, 0), - angle: radians(0), - halfWidth: 2, - halfHeight: 1, - }; + const target: Ellipse = ellipse(point(0, 0), radians(0), 2, 1); it("point on ellipse", () => { [point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => { - expect(pointOnEllipse(p, ellipse)).toBe(true); + expect(pointOnEllipse(p, target)).toBe(true); }); - expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(point(-1.4, 0.7), target, 0.1)).toBe(true); + expect(pointOnEllipse(point(-1.4, 0.71), target, 0.01)).toBe(true); - expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(point(1.4, 0.7), target, 0.1)).toBe(true); + expect(pointOnEllipse(point(1.4, 0.71), target, 0.01)).toBe(true); - expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(point(1, -0.86), target, 0.1)).toBe(true); + expect(pointOnEllipse(point(1, -0.86), target, 0.01)).toBe(true); - expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true); - expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true); + expect(pointOnEllipse(point(-1, -0.86), target, 0.1)).toBe(true); + expect(pointOnEllipse(point(-1, -0.86), target, 0.01)).toBe(true); - expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false); - expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false); + expect(pointOnEllipse(point(-1, 0.8), target)).toBe(false); + expect(pointOnEllipse(point(1, -0.8), target)).toBe(false); }); it("point in ellipse", () => { [point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => { - expect(pointInEllipse(p, ellipse)).toBe(true); + expect(pointInEllipse(p, target)).toBe(true); }); - expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true); - expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true); + expect(pointInEllipse(point(-1, 0.8), target)).toBe(true); + expect(pointInEllipse(point(1, -0.8), target)).toBe(true); - expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false); - expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false); + expect(pointInEllipse(point(-1, 1), target)).toBe(false); + expect(pointInEllipse(point(-1.4, 0.8), target)).toBe(false); + }); +}); + +describe("line and ellipse", () => { + it("detects outside segment", () => { + const l = lineSegment(point(-100, 0), point(-10, 0)); + const e = ellipse(point(0, 0), radians(0), 2, 2); + expect(interceptPointsOfLineAndEllipse(e, l).length).toBe(0); }); }); diff --git a/packages/math/ellipse.ts b/packages/math/ellipse.ts index b033d04944..9bcadbc09a 100644 --- a/packages/math/ellipse.ts +++ b/packages/math/ellipse.ts @@ -6,7 +6,7 @@ import { pointFromVector, pointRotateRads, } from "./point"; -import type { Ellipse, GenericPoint, Line } from "./types"; +import type { Ellipse, GenericPoint, LineSegment, Radians } from "./types"; import { PRECISION } from "./utils"; import { vector, @@ -16,6 +16,20 @@ import { vectorScale, } from "./vector"; +export function ellipse( + center: Point, + angle: Radians, + halfWidth: number, + halfHeight: number, +): Ellipse { + return { + center, + angle, + halfWidth, + halfHeight, + } as Ellipse; +} + export const pointInEllipse = ( p: Point, ellipse: Ellipse, @@ -162,19 +176,19 @@ const distanceToEllipse = ( * ellipse. */ export function interceptPointsOfLineAndEllipse( - ellipse: Readonly>, - l: Readonly>, + e: Readonly>, + l: Readonly>, ): Point[] { - const rx = ellipse.halfWidth; - const ry = ellipse.halfHeight; + const rx = e.halfWidth; + const ry = e.halfHeight; const nonRotatedLine = line( - pointRotateRads(l[0], ellipse.center, radians(-ellipse.angle)), - pointRotateRads(l[1], ellipse.center, radians(-ellipse.angle)), + pointRotateRads(l[0], e.center, radians(-e.angle)), + pointRotateRads(l[1], e.center, radians(-e.angle)), ); const dir = vectorFromPoint(nonRotatedLine[1], nonRotatedLine[0]); const diff = vector( - nonRotatedLine[0][0] - ellipse.center[0], - nonRotatedLine[0][1] - ellipse.center[1], + nonRotatedLine[0][0] - e.center[0], + nonRotatedLine[0][1] - e.center[1], ); const mDir = vector(dir[0] / (rx * rx), dir[1] / (ry * ry)); const mDiff = vector(diff[0] / (rx * rx), diff[1] / (ry * ry)); @@ -226,6 +240,6 @@ export function interceptPointsOfLineAndEllipse( } return intersections.map((point) => - pointRotateRads(point, ellipse.center, ellipse.angle), + pointRotateRads(point, e.center, e.angle), ); } diff --git a/packages/math/types.ts b/packages/math/types.ts index db582ecc4e..10c0932417 100644 --- a/packages/math/types.ts +++ b/packages/math/types.ts @@ -126,7 +126,8 @@ export type Curve = [ * Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle * corresponds to (1, 0) cartesian coordinates (point), i.e. to the "right" */ -export type SymmetricArc = { +export type SymmetricArc = { + center: Point; radius: number; startAngle: Radians; endAngle: Radians; @@ -152,4 +153,6 @@ export type Ellipse = { angle: Radians; halfWidth: number; halfHeight: number; +} & { + _brand: "excalimath_ellipse"; }; diff --git a/packages/utils/collision.ts b/packages/utils/collision.ts index 6d901c2ae3..4c67bc85e6 100644 --- a/packages/utils/collision.ts +++ b/packages/utils/collision.ts @@ -1,6 +1,6 @@ import type { Polycurve, Polyline } from "./geometry/shape"; import { type GeometricShape } from "./geometry/shape"; -import type { Curve, ViewportPoint } from "../math"; +import type { Curve, GenericPoint } from "../math"; import { lineSegment, point, @@ -8,23 +8,16 @@ import { pointOnLineSegment, pointOnPolygon, polygonFromPoints, - type GlobalPoint, - type LocalPoint, - type Polygon, pointOnEllipse, pointInEllipse, } from "../math"; // check if the given point is considered on the given shape's border -export const isPointOnShape = < - Point extends GlobalPoint | LocalPoint | ViewportPoint, ->( +export const isPointOnShape = ( point: Point, shape: GeometricShape, tolerance = 0, -) => { - // get the distance from the given point to the given element - // check if the distance is within the given epsilon range +): boolean => { switch (shape.type) { case "polygon": return pointOnPolygon(point, shape.data, tolerance); @@ -39,12 +32,12 @@ export const isPointOnShape = < case "polycurve": return pointOnPolycurve(point, shape.data, tolerance); default: - throw Error(`shape ${shape} is not implemented`); + throw Error(`Shape ${shape} is not implemented`); } }; // check if the given point is considered inside the element's border -export const isPointInShape = ( +export const isPointInShape = ( p: Point, shape: GeometricShape, ) => { @@ -69,17 +62,7 @@ export const isPointInShape = ( } }; -// check if the given element is in the given bounds -export const isPointInBounds = ( - point: Point, - bounds: Polygon, -) => { - return polygonIncludesPoint(point, bounds); -}; - -const pointOnPolycurve = < - Point extends LocalPoint | GlobalPoint | ViewportPoint, ->( +const pointOnPolycurve = ( point: Point, polycurve: Polycurve, tolerance: number, @@ -87,9 +70,7 @@ const pointOnPolycurve = < return polycurve.some((curve) => pointOnCurve(point, curve, tolerance)); }; -const cubicBezierEquation = < - Point extends LocalPoint | GlobalPoint | ViewportPoint, ->( +const cubicBezierEquation = ( curve: Curve, ) => { const [p0, p1, p2, p3] = curve; @@ -101,9 +82,7 @@ const cubicBezierEquation = < p0[idx] * Math.pow(t, 3); }; -const polyLineFromCurve = < - Point extends LocalPoint | GlobalPoint | ViewportPoint, ->( +const polyLineFromCurve = ( curve: Curve, segments = 10, ): Polyline => { @@ -125,9 +104,7 @@ const polyLineFromCurve = < return lineSegments; }; -export const pointOnCurve = < - Point extends LocalPoint | GlobalPoint | ViewportPoint, ->( +export const pointOnCurve = ( point: Point, curve: Curve, threshold: number, @@ -135,9 +112,7 @@ export const pointOnCurve = < return pointOnPolyline(point, polyLineFromCurve(curve), threshold); }; -export const pointOnPolyline = < - Point extends LocalPoint | GlobalPoint | ViewportPoint, ->( +export const pointOnPolyline = ( point: Point, polyline: Polyline, threshold = 10e-5, diff --git a/packages/utils/geometry/shape.ts b/packages/utils/geometry/shape.ts index b73c481401..1dc3d4e07e 100644 --- a/packages/utils/geometry/shape.ts +++ b/packages/utils/geometry/shape.ts @@ -23,6 +23,7 @@ import type { } from "../../math"; import { curve, + ellipse, lineSegment, point, pointFromArray, @@ -178,12 +179,12 @@ export const getEllipseShape = ( return { type: "ellipse", - data: { - center: point(x + width / 2, y + height / 2), + data: ellipse( + point(x + width / 2, y + height / 2), angle, - halfWidth: width / 2, - halfHeight: height / 2, - }, + width / 2, + height / 2, + ), }; };