diff --git a/excalidraw-app/components/DebugCanvas.tsx b/excalidraw-app/components/DebugCanvas.tsx index 15707c0e5a..9606d4e3ff 100644 --- a/excalidraw-app/components/DebugCanvas.tsx +++ b/excalidraw-app/components/DebugCanvas.tsx @@ -12,11 +12,7 @@ import { TrashIcon, } from "../../packages/excalidraw/components/icons"; import { STORAGE_KEYS } from "../app_constants"; -import { - isSegment, - type GlobalPoint, - type Segment, -} from "../../packages/math"; +import { isSegment, type GlobalPoint, type Segment } from "../../packages/math"; const renderLine = ( context: CanvasRenderingContext2D, diff --git a/packages/excalidraw/charts.ts b/packages/excalidraw/charts.ts index a6d9140c13..583120bcee 100644 --- a/packages/excalidraw/charts.ts +++ b/packages/excalidraw/charts.ts @@ -1,4 +1,3 @@ -import type { Radians } from "../math"; import { point, radians } from "../math"; import { COLOR_PALETTE, diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index 898d196a74..10fe103def 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -136,12 +136,7 @@ const getElementLineSegments = ( ).map((point) => pointRotateRads(point, center, element.angle)); if (element.type === "diamond") { - return [ - segment(n, w), - segment(n, e), - segment(s, w), - segment(s, e), - ]; + return [segment(n, w), segment(n, e), segment(s, w), segment(s, e)]; } if (element.type === "ellipse") { diff --git a/packages/math/angle.test.ts b/packages/math/angle.test.ts new file mode 100644 index 0000000000..74e9d679dc --- /dev/null +++ b/packages/math/angle.test.ts @@ -0,0 +1,13 @@ +import { cartesian2Polar, polar, radians } from "./angle"; +import { point } from "./point"; + +describe("cartesian to polar coordinate conversion", () => { + it("converts values properly", () => { + expect(cartesian2Polar(point(12, 5))).toEqual( + polar(13, radians(Math.atan(5 / 12))), + ); + expect(cartesian2Polar(point(5, 5))).toEqual( + polar(5 * Math.sqrt(2), radians(Math.PI / 4)), + ); + }); +}); diff --git a/packages/math/arc.test.ts b/packages/math/arc.test.ts index c3d13dc59f..98f5fefbbc 100644 --- a/packages/math/arc.test.ts +++ b/packages/math/arc.test.ts @@ -1,6 +1,7 @@ import { radians } from "./angle"; -import { arc, arcIncludesPoint } from "./arc"; +import { arc, arcIncludesPoint, arcSegmentInterceptPoint } from "./arc"; import { point } from "./point"; +import { segment } from "./segment"; describe("point on arc", () => { it("should detect point on simple arc", () => { @@ -28,3 +29,26 @@ describe("point on arc", () => { ).toBe(false); }); }); + +describe("intersection", () => { + it("should report correct interception point", () => { + expect( + arcSegmentInterceptPoint( + arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), + segment(point(2, 1), point(0, 0)), + ), + ).toEqual([point(0.894427190999916, 0.447213595499958)]); + }); + + it("should report both interception points when present", () => { + expect( + arcSegmentInterceptPoint( + arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), + segment(point(0.9, -2), point(0.9, 2)), + ), + ).toEqual([ + point(0.9, -0.4358898943540668), + point(0.9, 0.4358898943540668), + ]); + }); +}); diff --git a/packages/math/arc.ts b/packages/math/arc.ts index 699dae9c86..157cf68c51 100644 --- a/packages/math/arc.ts +++ b/packages/math/arc.ts @@ -6,11 +6,11 @@ import { PRECISION } from "./utils"; /** * Constructs a symmetric arc defined by the originating circle radius - * the start angle and end angle with 0 radians being the "northest" point + * the start angle and end angle with 0 radians being the most "eastward" point * of the circle. * * @param radius The radius of the circle this arc lies on - * @param startAngle The start angle with 0 radians being the "northest" point + * @param startAngle The start angle with 0 radians being the most "eastward" point * @param endAngle The end angle with 0 radians being the "northest" point * @returns The constructed symmetric arc */ @@ -46,20 +46,20 @@ export function arcIncludesPoint

( * Returns the intersection point(s) of a line segment represented by a start * point and end point and a symmetric arc. */ -export function interceptOfSymmetricArcAndSegment( +export function arcSegmentInterceptPoint( a: Readonly>, - l: Readonly>, + s: Readonly>, ): Point[] { return ellipseSegmentInterceptPoints( ellipse(a.center, radians(0), a.radius, a.radius), - l, + s, ).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 && + ? Math.abs(a.radius - candidateRadius) < PRECISION && 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 ea039c9ce0..ef3fc211f0 100644 --- a/packages/math/ellipse.test.ts +++ b/packages/math/ellipse.test.ts @@ -50,8 +50,31 @@ describe("point and ellipse", () => { describe("line and ellipse", () => { it("detects outside segment", () => { - const l = segment(point(-100, 0), point(-10, 0)); const e = ellipse(point(0, 0), radians(0), 2, 2); - expect(ellipseSegmentInterceptPoints(e, l).length).toBe(0); + + expect( + ellipseSegmentInterceptPoints( + e, + segment(point(-100, 0), point(-10, 0)), + ), + ).toEqual([]); + expect( + ellipseSegmentInterceptPoints( + e, + segment(point(-10, 0), point(10, 0)), + ), + ).toEqual([point(-2, 0), point(2, 0)]); + expect( + ellipseSegmentInterceptPoints( + e, + segment(point(-10, -2), point(10, -2)), + ), + ).toEqual([point(0, -2)]); + expect( + ellipseSegmentInterceptPoints( + e, + segment(point(0, -1), point(0, 1)), + ), + ).toEqual([]); }); }); diff --git a/packages/math/ellipse.ts b/packages/math/ellipse.ts index 4b9775b180..dd63cd814b 100644 --- a/packages/math/ellipse.ts +++ b/packages/math/ellipse.ts @@ -82,63 +82,10 @@ export const ellipseTouchesPoint = ( ellipse: Ellipse, threshold = PRECISION, ) => { - return distanceToEllipse(point, ellipse) <= threshold; + return ellipseDistance(point, ellipse) <= threshold; }; -export const ellipseFocusToCenter = ( - ellipse: Ellipse, -) => { - const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight; - - const majorAxis = widthGreaterThanHeight - ? ellipse.halfWidth * 2 - : ellipse.halfHeight * 2; - const minorAxis = widthGreaterThanHeight - ? ellipse.halfHeight * 2 - : ellipse.halfWidth * 2; - - return Math.sqrt(majorAxis ** 2 - minorAxis ** 2); -}; - -export const ellipseExtremes = ( - ellipse: Ellipse, -) => { - const { center, angle } = ellipse; - const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight; - - const majorAxis = widthGreaterThanHeight - ? ellipse.halfWidth * 2 - : ellipse.halfHeight * 2; - const minorAxis = widthGreaterThanHeight - ? ellipse.halfHeight * 2 - : ellipse.halfWidth * 2; - - const cos = Math.cos(angle); - const sin = Math.sin(angle); - - const sqSum = majorAxis ** 2 + minorAxis ** 2; - const sqDiff = (majorAxis ** 2 - minorAxis ** 2) * Math.cos(2 * angle); - - const yMax = Math.sqrt((sqSum - sqDiff) / 2); - const xAtYMax = - (yMax * sqSum * sin * cos) / - (majorAxis ** 2 * sin ** 2 + minorAxis ** 2 * cos ** 2); - - const xMax = Math.sqrt((sqSum + sqDiff) / 2); - const yAtXMax = - (xMax * sqSum * sin * cos) / - (majorAxis ** 2 * cos ** 2 + minorAxis ** 2 * sin ** 2); - const centerVector = vectorFromPoint(center); - - return [ - vectorAdd(vector(xAtYMax, yMax), centerVector), - vectorAdd(vectorScale(vector(xAtYMax, yMax), -1), centerVector), - vectorAdd(vector(xMax, yAtXMax), centerVector), - vectorAdd(vector(xMax, yAtXMax), centerVector), - ]; -}; - -const distanceToEllipse = ( +export const ellipseDistance = ( p: Point, ellipse: Ellipse, ) => { diff --git a/packages/math/segment.ts b/packages/math/segment.ts index 15cd181cde..d29e9c25fd 100644 --- a/packages/math/segment.ts +++ b/packages/math/segment.ts @@ -1,8 +1,10 @@ +import { invariant } from "../excalidraw/utils"; import { isPoint, pointCenter, pointFromVector, pointRotateRads, + pointsEqual, } from "./point"; import type { GenericPoint, Segment, Radians } from "./types"; import { PRECISION } from "./utils"; @@ -21,6 +23,11 @@ import { * @returns The line segment delineated by the points */ export function segment

(a: P, b: P): Segment

{ + invariant( + !pointsEqual(a, b), + "The start and end points of the segment cannot match", + ); + return [a, b] as Segment

; }