refactor: separate elements logic into a standalone package (#9285)
Some checks failed
Auto release excalidraw next / Auto-release-excalidraw-next (push) Failing after 2m36s
Build Docker image / build-docker (push) Failing after 6s
Cancel previous runs / cancel (push) Failing after 1s
Publish Docker / publish-docker (push) Failing after 31s
New Sentry production release / sentry (push) Failing after 2m3s

This commit is contained in:
Marcel Mraz 2025-03-26 15:24:59 +01:00 committed by GitHub
parent a18f059188
commit 432a46ef9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
372 changed files with 3466 additions and 2466 deletions

View file

@ -0,0 +1,102 @@
import "@excalidraw/utils/test-utils";
import {
curve,
curveClosestPoint,
curveIntersectLineSegment,
curvePointDistance,
} from "../src/curve";
import { pointFrom } from "../src/point";
import { lineSegment } from "../src/segment";
describe("Math curve", () => {
describe("line segment intersection", () => {
it("point is found when control points are the same", () => {
const c = curve(
pointFrom(100, 0),
pointFrom(100, 100),
pointFrom(100, 100),
pointFrom(0, 100),
);
const l = lineSegment(pointFrom(0, 0), pointFrom(200, 200));
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([
[87.5, 87.5],
]);
});
it("point is found when control points aren't the same", () => {
const c = curve(
pointFrom(100, 0),
pointFrom(100, 60),
pointFrom(60, 100),
pointFrom(0, 100),
);
const l = lineSegment(pointFrom(0, 0), pointFrom(200, 200));
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([
[72.5, 72.5],
]);
});
it("points are found when curve is sliced at 3 points", () => {
const c = curve(
pointFrom(-50, -50),
pointFrom(10, -50),
pointFrom(10, 50),
pointFrom(50, 50),
);
const l = lineSegment(pointFrom(0, 112.5), pointFrom(90, 0));
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([[50, 50]]);
});
it("can be detected where the determinant is overly precise", () => {
const c = curve(
pointFrom(41.028864759926016, 12.226249068355052),
pointFrom(41.028864759926016, 33.55958240168839),
pointFrom(30.362198093259348, 44.22624906835505),
pointFrom(9.028864759926016, 44.22624906835505),
);
const l = lineSegment(
pointFrom(-82.30963544324186, -41.19949363038283),
pointFrom(188.2149592542487, 134.75505940984908),
);
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([
[34.4, 34.71],
]);
});
});
describe("point closest to other", () => {
it("point can be found", () => {
const c = curve(
pointFrom(-50, -50),
pointFrom(10, -50),
pointFrom(10, 50),
pointFrom(50, 50),
);
const p = pointFrom(0, 0);
expect([curveClosestPoint(c, p)]).toCloselyEqualPoints([
[5.965462100367372, -3.04104878946646],
]);
});
});
describe("point shortest distance", () => {
it("can be determined", () => {
const c = curve(
pointFrom(-50, -50),
pointFrom(10, -50),
pointFrom(10, 50),
pointFrom(50, 50),
);
const p = pointFrom(0, 0);
expect(curvePointDistance(c, p)).toBeCloseTo(6.695873043213627);
});
});
});

View file

@ -0,0 +1,127 @@
import {
ellipse,
ellipseSegmentInterceptPoints,
ellipseIncludesPoint,
ellipseTouchesPoint,
ellipseLineIntersectionPoints,
} from "../src/ellipse";
import { line } from "../src/line";
import { pointFrom } from "../src/point";
import { lineSegment } from "../src/segment";
import type { Ellipse, GlobalPoint } from "../src/types";
describe("point and ellipse", () => {
it("point on ellipse", () => {
const target: Ellipse<GlobalPoint> = ellipse(pointFrom(1, 2), 2, 1);
[
pointFrom(1, 3),
pointFrom(1, 1),
pointFrom(3, 2),
pointFrom(-1, 2),
].forEach((p) => {
expect(ellipseTouchesPoint(p, target)).toBe(true);
});
expect(ellipseTouchesPoint(pointFrom(-0.4, 2.7), target, 0.1)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(-0.4, 2.71), target, 0.01)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(2.4, 2.7), target, 0.1)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(2.4, 2.71), target, 0.01)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(2, 1.14), target, 0.1)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(2, 1.14), target, 0.01)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(0, 1.14), target, 0.1)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(0, 1.14), target, 0.01)).toBe(true);
expect(ellipseTouchesPoint(pointFrom(0, 2.8), target)).toBe(false);
expect(ellipseTouchesPoint(pointFrom(2, 1.2), target)).toBe(false);
});
it("point in ellipse", () => {
const target: Ellipse<GlobalPoint> = ellipse(pointFrom(0, 0), 2, 1);
[
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(ellipseIncludesPoint(p, target)).toBe(true);
});
expect(ellipseIncludesPoint(pointFrom(-1, 0.8), target)).toBe(true);
expect(ellipseIncludesPoint(pointFrom(1, -0.8), target)).toBe(true);
// Point on outline
expect(ellipseIncludesPoint(pointFrom(2, 0), target)).toBe(true);
expect(ellipseIncludesPoint(pointFrom(-1, 1), target)).toBe(false);
expect(ellipseIncludesPoint(pointFrom(-1.4, 0.8), target)).toBe(false);
});
});
describe("segment and ellipse", () => {
it("detects outside segment", () => {
const e = ellipse(pointFrom(0, 0), 2, 2);
expect(
ellipseSegmentInterceptPoints(
e,
lineSegment<GlobalPoint>(pointFrom(-100, 0), pointFrom(-10, 0)),
),
).toEqual([]);
expect(
ellipseSegmentInterceptPoints(
e,
lineSegment<GlobalPoint>(pointFrom(-10, 0), pointFrom(10, 0)),
),
).toEqual([pointFrom(-2, 0), pointFrom(2, 0)]);
expect(
ellipseSegmentInterceptPoints(
e,
lineSegment<GlobalPoint>(pointFrom(-10, -2), pointFrom(10, -2)),
),
).toEqual([pointFrom(0, -2)]);
expect(
ellipseSegmentInterceptPoints(
e,
lineSegment<GlobalPoint>(pointFrom(0, -1), pointFrom(0, 1)),
),
).toEqual([]);
});
});
describe("line and ellipse", () => {
const e = ellipse(pointFrom(0, 0), 2, 2);
it("detects outside line", () => {
expect(
ellipseLineIntersectionPoints(
e,
line<GlobalPoint>(pointFrom(-10, -10), pointFrom(10, -10)),
),
).toEqual([]);
});
it("detects line intersecting ellipse", () => {
expect(
ellipseLineIntersectionPoints(
e,
line<GlobalPoint>(pointFrom(0, -1), pointFrom(0, 1)),
),
).toEqual([pointFrom(0, 2), pointFrom(0, -2)]);
expect(
ellipseLineIntersectionPoints(
e,
line<GlobalPoint>(pointFrom(-100, 0), pointFrom(-10, 0)),
).map(([x, y]) => pointFrom(Math.round(x), Math.round(y))),
).toEqual([pointFrom(2, 0), pointFrom(-2, 0)]);
});
it("detects line touching ellipse", () => {
expect(
ellipseLineIntersectionPoints(
e,
line<GlobalPoint>(pointFrom(-2, -2), pointFrom(2, -2)),
),
).toEqual([pointFrom(0, -2)]);
});
});

View file

@ -0,0 +1,31 @@
import { line, linesIntersectAt } from "../src/line";
import { pointFrom } from "../src/point";
describe("line-line intersections", () => {
it("should correctly detect intersection at origin", () => {
expect(
linesIntersectAt(
line(pointFrom(-5, -5), pointFrom(5, 5)),
line(pointFrom(5, -5), pointFrom(-5, 5)),
),
).toEqual(pointFrom(0, 0));
});
it("should correctly detect intersection at non-origin", () => {
expect(
linesIntersectAt(
line(pointFrom(0, 0), pointFrom(10, 10)),
line(pointFrom(10, 0), pointFrom(0, 10)),
),
).toEqual(pointFrom(5, 5));
});
it("should correctly detect parallel lines", () => {
expect(
linesIntersectAt(
line(pointFrom(0, 0), pointFrom(0, 10)),
line(pointFrom(10, 0), pointFrom(10, 10)),
),
).toBe(null);
});
});

View file

@ -0,0 +1,25 @@
import { pointFrom, pointRotateRads } from "../src/point";
import type { Radians } from "../src/types";
describe("rotate", () => {
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
const x1 = 10;
const y1 = 20;
const x2 = 20;
const y2 = 30;
const angle = (Math.PI / 2) as Radians;
const [rotatedX, rotatedY] = pointRotateRads(
pointFrom(x1, y1),
pointFrom(x2, y2),
angle,
);
expect([rotatedX, rotatedY]).toEqual([30, 20]);
const res2 = pointRotateRads(
pointFrom(rotatedX, rotatedY),
pointFrom(x2, y2),
-angle as Radians,
);
expect(res2).toEqual([x1, x2]);
});
});

View file

@ -0,0 +1,51 @@
import { rangeInclusive, rangeIntersection, rangesOverlap } from "../src/range";
describe("range overlap", () => {
const range1_4 = rangeInclusive(1, 4);
it("should overlap when range a contains range b", () => {
expect(rangesOverlap(range1_4, rangeInclusive(2, 3))).toBe(true);
expect(rangesOverlap(range1_4, range1_4)).toBe(true);
expect(rangesOverlap(range1_4, rangeInclusive(1, 3))).toBe(true);
expect(rangesOverlap(range1_4, rangeInclusive(2, 4))).toBe(true);
});
it("should overlap when range b contains range a", () => {
expect(rangesOverlap(rangeInclusive(2, 3), range1_4)).toBe(true);
expect(rangesOverlap(rangeInclusive(1, 3), range1_4)).toBe(true);
expect(rangesOverlap(rangeInclusive(2, 4), range1_4)).toBe(true);
});
it("should overlap when range a and b intersect", () => {
expect(rangesOverlap(range1_4, rangeInclusive(2, 5))).toBe(true);
});
});
describe("range intersection", () => {
const range1_4 = rangeInclusive(1, 4);
it("should intersect completely with itself", () => {
expect(rangeIntersection(range1_4, range1_4)).toEqual(range1_4);
});
it("should intersect irrespective of order", () => {
expect(rangeIntersection(range1_4, rangeInclusive(2, 3))).toEqual([2, 3]);
expect(rangeIntersection(rangeInclusive(2, 3), range1_4)).toEqual([2, 3]);
expect(rangeIntersection(range1_4, rangeInclusive(3, 5))).toEqual(
rangeInclusive(3, 4),
);
expect(rangeIntersection(rangeInclusive(3, 5), range1_4)).toEqual(
rangeInclusive(3, 4),
);
});
it("should intersect at the edge", () => {
expect(rangeIntersection(range1_4, rangeInclusive(4, 5))).toEqual(
rangeInclusive(4, 4),
);
});
it("should not intersect", () => {
expect(rangeIntersection(range1_4, rangeInclusive(5, 7))).toEqual(null);
});
});

View file

@ -0,0 +1,21 @@
import { pointFrom } from "../src/point";
import { lineSegment, lineSegmentIntersectionPoints } from "../src/segment";
describe("line-segment intersections", () => {
it("should correctly detect intersection", () => {
expect(
lineSegmentIntersectionPoints(
lineSegment(pointFrom(0, 0), pointFrom(5, 0)),
lineSegment(pointFrom(2, -2), pointFrom(3, 2)),
),
).toEqual(pointFrom(2.5, 0));
});
it("should correctly detect non-intersection", () => {
expect(
lineSegmentIntersectionPoints(
lineSegment(pointFrom(0, 0), pointFrom(5, 0)),
lineSegment(pointFrom(3, 1), pointFrom(4, 4)),
),
).toEqual(null);
});
});

View file

@ -0,0 +1,12 @@
import { isVector } from "../src/vector";
describe("Vector", () => {
test("isVector", () => {
expect(isVector([5, 5])).toBe(true);
expect(isVector([-5, -5])).toBe(true);
expect(isVector([5, 0.5])).toBe(true);
expect(isVector(null)).toBe(false);
expect(isVector(undefined)).toBe(false);
expect(isVector([5, NaN])).toBe(false);
});
});