,
-) {
- const indicesGroups: number[][] = [];
-
- let i = 0;
-
- while (i < elements.length) {
- if (movedElements.has(elements[i].id)) {
- const indicesGroup = [i - 1, i]; // push the lower bound index as the first item
-
- while (++i < elements.length) {
- if (!movedElements.has(elements[i].id)) {
- break;
- }
-
- indicesGroup.push(i);
- }
-
- indicesGroup.push(i); // push the upper bound index as the last item
- indicesGroups.push(indicesGroup);
- } else {
- i++;
- }
- }
-
- return indicesGroups;
-}
-
-/**
- * Gets contiguous groups of all invalid indices automatically detected inside the elements array.
- *
- * WARN: First and last items within the groups do NOT have to be contiguous, those are the found lower and upper bounds!
- */
-function getInvalidIndicesGroups(elements: readonly ExcalidrawElement[]) {
- const indicesGroups: number[][] = [];
-
- // once we find lowerBound / upperBound, it cannot be lower than that, so we cache it for better perf.
- let lowerBound: ExcalidrawElement["index"] | undefined = undefined;
- let upperBound: ExcalidrawElement["index"] | undefined = undefined;
- let lowerBoundIndex: number = -1;
- let upperBoundIndex: number = 0;
-
- /** @returns maybe valid lowerBound */
- const getLowerBound = (
- index: number,
- ): [ExcalidrawElement["index"] | undefined, number] => {
- const lowerBound = elements[lowerBoundIndex]
- ? elements[lowerBoundIndex].index
- : undefined;
-
- // we are already iterating left to right, therefore there is no need for additional looping
- const candidate = elements[index - 1]?.index;
-
- if (
- (!lowerBound && candidate) || // first lowerBound
- (lowerBound && candidate && candidate > lowerBound) // next lowerBound
- ) {
- // WARN: candidate's index could be higher or same as the current element's index
- return [candidate, index - 1];
- }
-
- // cache hit! take the last lower bound
- return [lowerBound, lowerBoundIndex];
- };
-
- /** @returns always valid upperBound */
- const getUpperBound = (
- index: number,
- ): [ExcalidrawElement["index"] | undefined, number] => {
- const upperBound = elements[upperBoundIndex]
- ? elements[upperBoundIndex].index
- : undefined;
-
- // cache hit! don't let it find the upper bound again
- if (upperBound && index < upperBoundIndex) {
- return [upperBound, upperBoundIndex];
- }
-
- // set the current upperBoundIndex as the starting point
- let i = upperBoundIndex;
- while (++i < elements.length) {
- const candidate = elements[i]?.index;
-
- if (
- (!upperBound && candidate) || // first upperBound
- (upperBound && candidate && candidate > upperBound) // next upperBound
- ) {
- return [candidate, i];
- }
- }
-
- // we reached the end, sky is the limit
- return [undefined, i];
- };
-
- let i = 0;
-
- while (i < elements.length) {
- const current = elements[i].index;
- [lowerBound, lowerBoundIndex] = getLowerBound(i);
- [upperBound, upperBoundIndex] = getUpperBound(i);
-
- if (!isValidFractionalIndex(current, lowerBound, upperBound)) {
- // push the lower bound index as the first item
- const indicesGroup = [lowerBoundIndex, i];
-
- while (++i < elements.length) {
- const current = elements[i].index;
- const [nextLowerBound, nextLowerBoundIndex] = getLowerBound(i);
- const [nextUpperBound, nextUpperBoundIndex] = getUpperBound(i);
-
- if (isValidFractionalIndex(current, nextLowerBound, nextUpperBound)) {
- break;
- }
-
- // assign bounds only for the moved elements
- [lowerBound, lowerBoundIndex] = [nextLowerBound, nextLowerBoundIndex];
- [upperBound, upperBoundIndex] = [nextUpperBound, nextUpperBoundIndex];
-
- indicesGroup.push(i);
- }
-
- // push the upper bound index as the last item
- indicesGroup.push(upperBoundIndex);
- indicesGroups.push(indicesGroup);
- } else {
- i++;
- }
- }
-
- return indicesGroups;
-}
-
-function isValidFractionalIndex(
- index: ExcalidrawElement["index"] | undefined,
- predecessor: ExcalidrawElement["index"] | undefined,
- successor: ExcalidrawElement["index"] | undefined,
-) {
- if (!index) {
- return false;
- }
-
- if (predecessor && successor) {
- return predecessor < index && index < successor;
- }
-
- if (!predecessor && successor) {
- // first element
- return index < successor;
- }
-
- if (predecessor && !successor) {
- // last element
- return predecessor < index;
- }
-
- // only element in the array
- return !!index;
-}
-
-function generateIndices(
- elements: readonly ExcalidrawElement[],
- indicesGroups: number[][],
-) {
- const elementsUpdates = new Map<
- ExcalidrawElement,
- { index: FractionalIndex }
- >();
-
- for (const indices of indicesGroups) {
- const lowerBoundIndex = indices.shift()!;
- const upperBoundIndex = indices.pop()!;
-
- const fractionalIndices = generateNKeysBetween(
- elements[lowerBoundIndex]?.index,
- elements[upperBoundIndex]?.index,
- indices.length,
- ) as FractionalIndex[];
-
- for (let i = 0; i < indices.length; i++) {
- const element = elements[indices[i]];
-
- elementsUpdates.set(element, {
- index: fractionalIndices[i],
- });
- }
- }
-
- return elementsUpdates;
-}
-
-function isOrderedElement(
- element: ExcalidrawElement,
-): element is OrderedExcalidrawElement {
- // for now it's sufficient whether the index is there
- // meaning, the element was already ordered in the past
- // meaning, it is not a newly inserted element, not an unrestored element, etc.
- // it does not have to mean that the index itself is valid
- if (element.index) {
- return true;
- }
-
- return false;
-}
diff --git a/packages/fractional-index/src/index.ts b/packages/fractional-index/src/index.ts
deleted file mode 100644
index 249e4f100..000000000
--- a/packages/fractional-index/src/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export {
- validateFractionalIndices,
- orderByFractionalIndex,
- syncMovedIndices,
- syncInvalidIndices,
-} from "./fractionalIndex";
diff --git a/packages/fractional-index/tsconfig.json b/packages/fractional-index/tsconfig.json
deleted file mode 100644
index 5fb40d38c..000000000
--- a/packages/fractional-index/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "compilerOptions": {
- "target": "ESNext",
- "strict": true,
- "outDir": "dist/types",
- "skipLibCheck": true,
- "declaration": true,
- "emitDeclarationOnly": true,
- "allowSyntheticDefaultImports": true,
- "module": "ESNext",
- "moduleResolution": "Node",
- },
- "exclude": [
- "**/*.test.*",
- "**/tests/*",
- "types",
- "dist",
- ],
-}
diff --git a/packages/math/CHANGELOG.md b/packages/math/CHANGELOG.md
deleted file mode 100644
index e69de29bb..000000000
diff --git a/packages/math/README.md b/packages/math/README.md
index eaa163037..348a9de07 100644
--- a/packages/math/README.md
+++ b/packages/math/README.md
@@ -17,5 +17,3 @@ With PNPM, similarly install the package with this command:
```bash
pnpm add @excalidraw/math
```
-
-## API
diff --git a/packages/math/arc.test.ts b/packages/math/arc.test.ts
deleted file mode 100644
index adf778591..000000000
--- a/packages/math/arc.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { isPointOnSymmetricArc } from "./arc";
-import { pointFrom } from "./point";
-
-describe("point on arc", () => {
- it("should detect point on simple arc", () => {
- expect(
- isPointOnSymmetricArc(
- {
- radius: 1,
- startAngle: -Math.PI / 4,
- endAngle: Math.PI / 4,
- },
- pointFrom(0.92291667, 0.385),
- ),
- ).toBe(true);
- });
- it("should not detect point outside of a simple arc", () => {
- expect(
- isPointOnSymmetricArc(
- {
- radius: 1,
- startAngle: -Math.PI / 4,
- endAngle: Math.PI / 4,
- },
- pointFrom(-0.92291667, 0.385),
- ),
- ).toBe(false);
- });
- it("should not detect point with good angle but incorrect radius", () => {
- expect(
- isPointOnSymmetricArc(
- {
- radius: 1,
- startAngle: -Math.PI / 4,
- endAngle: Math.PI / 4,
- },
- pointFrom(-0.5, 0.5),
- ),
- ).toBe(false);
- });
-});
diff --git a/packages/math/arc.ts b/packages/math/arc.ts
deleted file mode 100644
index c93830dba..000000000
--- a/packages/math/arc.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { cartesian2Polar } from "./angle";
-import type { GlobalPoint, LocalPoint, SymmetricArc } from "./types";
-import { PRECISION } from "./utils";
-
-/**
- * Determines if a cartesian point lies on a symmetric arc, i.e. an arc which
- * is part of a circle contour centered on 0, 0.
- */
-export const isPointOnSymmetricArc = (
- { radius: arcRadius, startAngle, endAngle }: SymmetricArc,
- point: P,
-): boolean => {
- const [radius, angle] = cartesian2Polar(point);
-
- return startAngle < endAngle
- ? Math.abs(radius - arcRadius) < PRECISION &&
- startAngle <= angle &&
- endAngle >= angle
- : startAngle <= angle || endAngle >= angle;
-};
diff --git a/packages/math/curve.ts b/packages/math/curve.ts
deleted file mode 100644
index 68a885fd8..000000000
--- a/packages/math/curve.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import { pointFrom, pointRotateRads } from "./point";
-import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
-
-/**
- *
- * @param a
- * @param b
- * @param c
- * @param d
- * @returns
- */
-export function curve(
- a: Point,
- b: Point,
- c: Point,
- d: Point,
-) {
- return [a, b, c, d] as Curve;
-}
-
-export const curveRotate = (
- curve: Curve,
- angle: Radians,
- origin: Point,
-) => {
- return curve.map((p) => pointRotateRads(p, origin, angle));
-};
-
-/**
- *
- * @param pointsIn
- * @param curveTightness
- * @returns
- */
-export function curveToBezier(
- pointsIn: readonly Point[],
- curveTightness = 0,
-): Point[] {
- const len = pointsIn.length;
- if (len < 3) {
- throw new Error("A curve must have at least three points.");
- }
- const out: Point[] = [];
- if (len === 3) {
- out.push(
- pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
- pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
- pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
- pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
- );
- } else {
- const points: Point[] = [];
- points.push(pointsIn[0], pointsIn[0]);
- for (let i = 1; i < pointsIn.length; i++) {
- points.push(pointsIn[i]);
- if (i === pointsIn.length - 1) {
- points.push(pointsIn[i]);
- }
- }
- const b: Point[] = [];
- const s = 1 - curveTightness;
- out.push(pointFrom(points[0][0], points[0][1]));
- for (let i = 1; i + 2 < points.length; i++) {
- const cachedVertArray = points[i];
- b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]);
- b[1] = pointFrom(
- cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
- cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
- );
- b[2] = pointFrom(
- points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
- points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
- );
- b[3] = pointFrom(points[i + 1][0], points[i + 1][1]);
- out.push(b[1], b[2], b[3]);
- }
- }
- return out;
-}
-
-/**
- *
- * @param t
- * @param controlPoints
- * @returns
- */
-export const cubicBezierPoint = (
- t: number,
- controlPoints: Curve,
-): Point => {
- const [p0, p1, p2, p3] = controlPoints;
-
- const x =
- Math.pow(1 - t, 3) * p0[0] +
- 3 * Math.pow(1 - t, 2) * t * p1[0] +
- 3 * (1 - t) * Math.pow(t, 2) * p2[0] +
- Math.pow(t, 3) * p3[0];
-
- const y =
- Math.pow(1 - t, 3) * p0[1] +
- 3 * Math.pow(1 - t, 2) * t * p1[1] +
- 3 * (1 - t) * Math.pow(t, 2) * p2[1] +
- Math.pow(t, 3) * p3[1];
-
- return pointFrom(x, y);
-};
-
-/**
- *
- * @param point
- * @param controlPoints
- * @returns
- */
-export const cubicBezierDistance = (
- point: Point,
- controlPoints: Curve,
-) => {
- // Calculate the closest point on the Bezier curve to the given point
- const t = findClosestParameter(point, controlPoints);
-
- // Calculate the coordinates of the closest point on the curve
- const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
-
- // Calculate the distance between the given point and the closest point on the curve
- const distance = Math.sqrt(
- (point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
- );
-
- return distance;
-};
-
-const solveCubic = (a: number, b: number, c: number, d: number) => {
- // This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
- const roots: number[] = [];
-
- const discriminant =
- 18 * a * b * c * d -
- 4 * Math.pow(b, 3) * d +
- Math.pow(b, 2) * Math.pow(c, 2) -
- 4 * a * Math.pow(c, 3) -
- 27 * Math.pow(a, 2) * Math.pow(d, 2);
-
- if (discriminant >= 0) {
- const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
- const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
-
- const root1 = (-b - C - D) / (3 * a);
- const root2 = (-b + (C + D) / 2) / (3 * a);
- const root3 = (-b + (C + D) / 2) / (3 * a);
-
- roots.push(root1, root2, root3);
- } else {
- const realPart = -b / (3 * a);
-
- const root1 =
- 2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
- const root2 =
- 2 *
- Math.sqrt(-b / (3 * a)) *
- Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
- const root3 =
- 2 *
- Math.sqrt(-b / (3 * a)) *
- Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
-
- roots.push(root1, root2, root3);
- }
-
- return roots;
-};
-
-const findClosestParameter = (
- point: Point,
- controlPoints: Curve,
-) => {
- // This function finds the parameter t that minimizes the distance between the point
- // and any point on the cubic Bezier curve.
-
- const [p0, p1, p2, p3] = controlPoints;
-
- // Use the direct formula to find the parameter t
- const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
- const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
- const c = 3 * p1[0] - 3 * p0[0];
- const d = p0[0] - point[0];
-
- const rootsX = solveCubic(a, b, c, d);
-
- // Do the same for the y-coordinate
- const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
- const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
- const g = 3 * p1[1] - 3 * p0[1];
- const h = p0[1] - point[1];
-
- const rootsY = solveCubic(e, f, g, h);
-
- // Select the real root that is between 0 and 1 (inclusive)
- const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
- const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
-
- if (validRootsX.length === 0 || validRootsY.length === 0) {
- // No valid roots found, use the midpoint as a fallback
- return 0.5;
- }
-
- // Choose the parameter t that minimizes the distance
- let minDistance = Infinity;
- let closestT = 0;
-
- for (const rootX of validRootsX) {
- for (const rootY of validRootsY) {
- const distance = Math.sqrt(
- (rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
- );
- if (distance < minDistance) {
- minDistance = distance;
- closestT = (rootX + rootY) / 2; // Use the average for a smoother result
- }
- }
- }
-
- return closestT;
-};
diff --git a/packages/math/ga/ga.test.ts b/packages/math/ga/ga.test.ts
deleted file mode 100644
index 767b5b65b..000000000
--- a/packages/math/ga/ga.test.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import * as GA from "./ga";
-import { point, toString, direction, offset } from "./ga";
-import * as GAPoint from "./gapoints";
-import * as GALine from "./galines";
-import * as GATransform from "./gatransforms";
-
-describe("geometric algebra", () => {
- describe("points", () => {
- it("distanceToLine", () => {
- const point = GA.point(3, 3);
- const line = GALine.equation(0, 1, -1);
- expect(GAPoint.distanceToLine(point, line)).toEqual(2);
- });
-
- it("distanceToLine neg", () => {
- const point = GA.point(-3, -3);
- const line = GALine.equation(0, 1, -1);
- expect(GAPoint.distanceToLine(point, line)).toEqual(-4);
- });
- });
- describe("lines", () => {
- it("through", () => {
- const a = GA.point(0, 0);
- const b = GA.point(2, 0);
- expect(toString(GALine.through(a, b))).toEqual(
- toString(GALine.equation(0, 2, 0)),
- );
- });
- it("parallel", () => {
- const point = GA.point(3, 3);
- const line = GALine.equation(0, 1, -1);
- const parallel = GALine.parallel(line, 2);
- expect(GAPoint.distanceToLine(point, parallel)).toEqual(0);
- });
- });
-
- describe("translation", () => {
- it("points", () => {
- const start = point(2, 2);
- const move = GATransform.translation(direction(0, 1));
- const end = GATransform.apply(move, start);
- expect(toString(end)).toEqual(toString(point(2, 3)));
- });
-
- it("points 2", () => {
- const start = point(2, 2);
- const move = GATransform.translation(offset(3, 4));
- const end = GATransform.apply(move, start);
- expect(toString(end)).toEqual(toString(point(5, 6)));
- });
-
- it("lines", () => {
- const original = GALine.through(point(2, 2), point(3, 4));
- const move = GATransform.translation(offset(3, 4));
- const parallel = GATransform.apply(move, original);
- expect(toString(parallel)).toEqual(
- toString(GALine.through(point(5, 6), point(6, 8))),
- );
- });
- });
- describe("rotation", () => {
- it("points", () => {
- const start = point(2, 2);
- const pivot = point(1, 1);
- const rotate = GATransform.rotation(pivot, Math.PI / 2);
- const end = GATransform.apply(rotate, start);
- expect(toString(end)).toEqual(toString(point(2, 0)));
- });
- });
-});
diff --git a/packages/math/ga/ga.ts b/packages/math/ga/ga.ts
deleted file mode 100644
index 271aa7ae9..000000000
--- a/packages/math/ga/ga.ts
+++ /dev/null
@@ -1,317 +0,0 @@
-/**
- * This is a 2D Projective Geometric Algebra implementation.
- *
- * For wider context on geometric algebra visit see https://bivector.net.
- *
- * For this specific algebra see cheatsheet https://bivector.net/2DPGA.pdf.
- *
- * Converted from generator written by enki, with a ton of added on top.
- *
- * This library uses 8-vectors to represent points, directions and lines
- * in 2D space.
- *
- * An array `[a, b, c, d, e, f, g, h]` represents a n(8)vector:
- * a + b*e0 + c*e1 + d*e2 + e*e01 + f*e20 + g*e12 + h*e012
- *
- * See GAPoint, GALine, GADirection and GATransform modules for common
- * operations.
- */
-
-export type Point = NVector;
-export type Direction = NVector;
-export type Line = NVector;
-export type Transform = NVector;
-
-export const point = (x: number, y: number): Point => [0, 0, 0, 0, y, x, 1, 0];
-
-export const origin = (): Point => [0, 0, 0, 0, 0, 0, 1, 0];
-
-export const direction = (x: number, y: number): Direction => {
- const norm = Math.hypot(x, y); // same as `inorm(direction(x, y))`
- return [0, 0, 0, 0, y / norm, x / norm, 0, 0];
-};
-
-export const offset = (x: number, y: number): Direction => [
- 0,
- 0,
- 0,
- 0,
- y,
- x,
- 0,
- 0,
-];
-
-/// This is the "implementation" part of the library
-
-type NVector = readonly [
- number,
- number,
- number,
- number,
- number,
- number,
- number,
- number,
-];
-
-// These are labels for what each number in an nvector represents
-const NVECTOR_BASE = ["1", "e0", "e1", "e2", "e01", "e20", "e12", "e012"];
-
-// Used to represent points, lines and transformations
-export const nvector = (value: number = 0, index: number = 0): NVector => {
- const result = [0, 0, 0, 0, 0, 0, 0, 0];
- if (index < 0 || index > 7) {
- throw new Error(`Expected \`index\` between 0 and 7, got \`${index}\``);
- }
- if (value !== 0) {
- result[index] = value;
- }
- return result as unknown as NVector;
-};
-
-const STRING_EPSILON = 0.000001;
-export const toString = (nvector: NVector): string => {
- const result = nvector
- .map((value, index) =>
- Math.abs(value) > STRING_EPSILON
- ? value.toFixed(7).replace(/(\.|0+)$/, "") +
- (index > 0 ? NVECTOR_BASE[index] : "")
- : null,
- )
- .filter((representation) => representation != null)
- .join(" + ");
- return result === "" ? "0" : result;
-};
-
-// Reverse the order of the basis blades.
-export const reverse = (nvector: NVector): NVector => [
- nvector[0],
- nvector[1],
- nvector[2],
- nvector[3],
- -nvector[4],
- -nvector[5],
- -nvector[6],
- -nvector[7],
-];
-
-// Poincare duality operator.
-export const dual = (nvector: NVector): NVector => [
- nvector[7],
- nvector[6],
- nvector[5],
- nvector[4],
- nvector[3],
- nvector[2],
- nvector[1],
- nvector[0],
-];
-
-// Clifford Conjugation
-export const conjugate = (nvector: NVector): NVector => [
- nvector[0],
- -nvector[1],
- -nvector[2],
- -nvector[3],
- -nvector[4],
- -nvector[5],
- -nvector[6],
- nvector[7],
-];
-
-// Main involution
-export const involute = (nvector: NVector): NVector => [
- nvector[0],
- -nvector[1],
- -nvector[2],
- -nvector[3],
- nvector[4],
- nvector[5],
- nvector[6],
- -nvector[7],
-];
-
-// Multivector addition
-export const add = (a: NVector, b: NVector | number): NVector => {
- if (isNumber(b)) {
- return [a[0] + b, a[1], a[2], a[3], a[4], a[5], a[6], a[7]];
- }
- return [
- a[0] + b[0],
- a[1] + b[1],
- a[2] + b[2],
- a[3] + b[3],
- a[4] + b[4],
- a[5] + b[5],
- a[6] + b[6],
- a[7] + b[7],
- ];
-};
-
-// Multivector subtraction
-export const sub = (a: NVector, b: NVector | number): NVector => {
- if (isNumber(b)) {
- return [a[0] - b, a[1], a[2], a[3], a[4], a[5], a[6], a[7]];
- }
- return [
- a[0] - b[0],
- a[1] - b[1],
- a[2] - b[2],
- a[3] - b[3],
- a[4] - b[4],
- a[5] - b[5],
- a[6] - b[6],
- a[7] - b[7],
- ];
-};
-
-// The geometric product.
-export const mul = (a: NVector, b: NVector | number): NVector => {
- if (isNumber(b)) {
- return [
- a[0] * b,
- a[1] * b,
- a[2] * b,
- a[3] * b,
- a[4] * b,
- a[5] * b,
- a[6] * b,
- a[7] * b,
- ];
- }
- return [
- mulScalar(a, b),
- b[1] * a[0] +
- b[0] * a[1] -
- b[4] * a[2] +
- b[5] * a[3] +
- b[2] * a[4] -
- b[3] * a[5] -
- b[7] * a[6] -
- b[6] * a[7],
- b[2] * a[0] + b[0] * a[2] - b[6] * a[3] + b[3] * a[6],
- b[3] * a[0] + b[6] * a[2] + b[0] * a[3] - b[2] * a[6],
- b[4] * a[0] +
- b[2] * a[1] -
- b[1] * a[2] +
- b[7] * a[3] +
- b[0] * a[4] +
- b[6] * a[5] -
- b[5] * a[6] +
- b[3] * a[7],
- b[5] * a[0] -
- b[3] * a[1] +
- b[7] * a[2] +
- b[1] * a[3] -
- b[6] * a[4] +
- b[0] * a[5] +
- b[4] * a[6] +
- b[2] * a[7],
- b[6] * a[0] + b[3] * a[2] - b[2] * a[3] + b[0] * a[6],
- b[7] * a[0] +
- b[6] * a[1] +
- b[5] * a[2] +
- b[4] * a[3] +
- b[3] * a[4] +
- b[2] * a[5] +
- b[1] * a[6] +
- b[0] * a[7],
- ];
-};
-
-export const mulScalar = (a: NVector, b: NVector): number =>
- b[0] * a[0] + b[2] * a[2] + b[3] * a[3] - b[6] * a[6];
-
-// The outer/exterior/wedge product.
-export const meet = (a: NVector, b: NVector): NVector => [
- b[0] * a[0],
- b[1] * a[0] + b[0] * a[1],
- b[2] * a[0] + b[0] * a[2],
- b[3] * a[0] + b[0] * a[3],
- b[4] * a[0] + b[2] * a[1] - b[1] * a[2] + b[0] * a[4],
- b[5] * a[0] - b[3] * a[1] + b[1] * a[3] + b[0] * a[5],
- b[6] * a[0] + b[3] * a[2] - b[2] * a[3] + b[0] * a[6],
- b[7] * a[0] +
- b[6] * a[1] +
- b[5] * a[2] +
- b[4] * a[3] +
- b[3] * a[4] +
- b[2] * a[5] +
- b[1] * a[6],
-];
-
-// The regressive product.
-export const join = (a: NVector, b: NVector): NVector => [
- joinScalar(a, b),
- a[1] * b[7] + a[4] * b[5] - a[5] * b[4] + a[7] * b[1],
- a[2] * b[7] - a[4] * b[6] + a[6] * b[4] + a[7] * b[2],
- a[3] * b[7] + a[5] * b[6] - a[6] * b[5] + a[7] * b[3],
- a[4] * b[7] + a[7] * b[4],
- a[5] * b[7] + a[7] * b[5],
- a[6] * b[7] + a[7] * b[6],
- a[7] * b[7],
-];
-
-export const joinScalar = (a: NVector, b: NVector): number =>
- a[0] * b[7] +
- a[1] * b[6] +
- a[2] * b[5] +
- a[3] * b[4] +
- a[4] * b[3] +
- a[5] * b[2] +
- a[6] * b[1] +
- a[7] * b[0];
-
-// The inner product.
-export const dot = (a: NVector, b: NVector): NVector => [
- b[0] * a[0] + b[2] * a[2] + b[3] * a[3] - b[6] * a[6],
- b[1] * a[0] +
- b[0] * a[1] -
- b[4] * a[2] +
- b[5] * a[3] +
- b[2] * a[4] -
- b[3] * a[5] -
- b[7] * a[6] -
- b[6] * a[7],
- b[2] * a[0] + b[0] * a[2] - b[6] * a[3] + b[3] * a[6],
- b[3] * a[0] + b[6] * a[2] + b[0] * a[3] - b[2] * a[6],
- b[4] * a[0] + b[7] * a[3] + b[0] * a[4] + b[3] * a[7],
- b[5] * a[0] + b[7] * a[2] + b[0] * a[5] + b[2] * a[7],
- b[6] * a[0] + b[0] * a[6],
- b[7] * a[0] + b[0] * a[7],
-];
-
-export const norm = (a: NVector): number =>
- Math.sqrt(Math.abs(a[0] * a[0] - a[2] * a[2] - a[3] * a[3] + a[6] * a[6]));
-
-export const inorm = (a: NVector): number =>
- Math.sqrt(Math.abs(a[7] * a[7] - a[5] * a[5] - a[4] * a[4] + a[1] * a[1]));
-
-export const normalized = (a: NVector): NVector => {
- const n = norm(a);
- if (n === 0 || n === 1) {
- return a;
- }
- const sign = a[6] < 0 ? -1 : 1;
- return mul(a, sign / n);
-};
-
-export const inormalized = (a: NVector): NVector => {
- const n = inorm(a);
- if (n === 0 || n === 1) {
- return a;
- }
- return mul(a, 1 / n);
-};
-
-const isNumber = (a: any): a is number => typeof a === "number";
-
-export const E0: NVector = nvector(1, 1);
-export const E1: NVector = nvector(1, 2);
-export const E2: NVector = nvector(1, 3);
-export const E01: NVector = nvector(1, 4);
-export const E20: NVector = nvector(1, 5);
-export const E12: NVector = nvector(1, 6);
-export const E012: NVector = nvector(1, 7);
-export const I = E012;
diff --git a/packages/math/ga/gadirections.ts b/packages/math/ga/gadirections.ts
deleted file mode 100644
index 2f631fa6a..000000000
--- a/packages/math/ga/gadirections.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import * as GA from "./ga";
-import type { Line, Direction, Point } from "./ga";
-
-/**
- * A direction is stored as an array `[0, 0, 0, 0, y, x, 0, 0]` representing
- * vector `(x, y)`.
- */
-
-export const from = (point: Point): Point => [
- 0,
- 0,
- 0,
- 0,
- point[4],
- point[5],
- 0,
- 0,
-];
-
-export const fromTo = (from: Point, to: Point): Direction =>
- GA.inormalized([0, 0, 0, 0, to[4] - from[4], to[5] - from[5], 0, 0]);
-
-export const orthogonal = (direction: Direction): Direction =>
- GA.inormalized([0, 0, 0, 0, -direction[5], direction[4], 0, 0]);
-
-export const orthogonalToLine = (line: Line): Direction => GA.mul(line, GA.I);
diff --git a/packages/math/ga/galines.ts b/packages/math/ga/galines.ts
deleted file mode 100644
index f5058ce69..000000000
--- a/packages/math/ga/galines.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import * as GA from "./ga";
-import type { Line, Point } from "./ga";
-
-/**
- * A line is stored as an array `[0, c, a, b, 0, 0, 0, 0]` representing:
- * c * e0 + a * e1 + b*e2
- *
- * This maps to a standard formula `a * x + b * y + c`.
- *
- * `(-b, a)` corresponds to a 2D vector parallel to the line. The lines
- * have a natural orientation, corresponding to that vector.
- *
- * The magnitude ("norm") of the line is `sqrt(a ^ 2 + b ^ 2)`.
- * `c / norm(line)` is the oriented distance from line to origin.
- */
-
-// Returns line with direction (x, y) through origin
-export const vector = (x: number, y: number): Line =>
- GA.normalized([0, 0, -y, x, 0, 0, 0, 0]);
-
-// For equation ax + by + c = 0.
-export const equation = (a: number, b: number, c: number): Line =>
- GA.normalized([0, c, a, b, 0, 0, 0, 0]);
-
-export const through = (from: Point, to: Point): Line =>
- GA.normalized(GA.join(to, from));
-
-export const orthogonal = (line: Line, point: Point): Line =>
- GA.dot(line, point);
-
-// Returns a line perpendicular to the line through `against` and `intersection`
-// going through `intersection`.
-export const orthogonalThrough = (against: Point, intersection: Point): Line =>
- orthogonal(through(against, intersection), intersection);
-
-export const parallel = (line: Line, distance: number): Line => {
- const result = line.slice();
- result[1] -= distance;
- return result as unknown as Line;
-};
-
-export const parallelThrough = (line: Line, point: Point): Line =>
- orthogonal(orthogonal(point, line), point);
-
-export const distance = (line1: Line, line2: Line): number =>
- GA.inorm(GA.meet(line1, line2));
-
-export const angle = (line1: Line, line2: Line): number =>
- Math.acos(GA.dot(line1, line2)[0]);
-
-// The orientation of the line
-export const sign = (line: Line): number => Math.sign(line[1]);
diff --git a/packages/math/ga/gapoints.ts b/packages/math/ga/gapoints.ts
deleted file mode 100644
index 909e8ffe6..000000000
--- a/packages/math/ga/gapoints.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import * as GA from "./ga";
-import * as GALine from "./galines";
-import type { Point, Line } from "./ga";
-import { join } from "./ga";
-
-export const from = ([x, y]: readonly [number, number]): Point => [
- 0,
- 0,
- 0,
- 0,
- y,
- x,
- 1,
- 0,
-];
-
-export const toTuple = (point: Point): [number, number] => [point[5], point[4]];
-
-export const abs = (point: Point): Point => [
- 0,
- 0,
- 0,
- 0,
- Math.abs(point[4]),
- Math.abs(point[5]),
- 1,
- 0,
-];
-
-export const intersect = (line1: Line, line2: Line): Point =>
- GA.normalized(GA.meet(line1, line2));
-
-// Projects `point` onto the `line`.
-// The returned point is the closest point on the `line` to the `point`.
-export const project = (point: Point, line: Line): Point =>
- intersect(GALine.orthogonal(line, point), line);
-
-export const distance = (point1: Point, point2: Point): number =>
- GA.norm(join(point1, point2));
-
-export const distanceToLine = (point: Point, line: Line): number =>
- GA.joinScalar(point, line);
diff --git a/packages/math/ga/gatransforms.ts b/packages/math/ga/gatransforms.ts
deleted file mode 100644
index 2301d979e..000000000
--- a/packages/math/ga/gatransforms.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as GA from "./ga";
-import type { Line, Direction, Point, Transform } from "./ga";
-import * as GADirection from "./gadirections";
-
-/**
- * TODO: docs
- */
-
-export const rotation = (pivot: Point, angle: number): Transform =>
- GA.add(GA.mul(pivot, Math.sin(angle / 2)), Math.cos(angle / 2));
-
-export const translation = (direction: Direction): Transform => [
- 1,
- 0,
- 0,
- 0,
- -(0.5 * direction[5]),
- 0.5 * direction[4],
- 0,
- 0,
-];
-
-export const translationOrthogonal = (
- direction: Direction,
- distance: number,
-): Transform => {
- const scale = 0.5 * distance;
- return [1, 0, 0, 0, scale * direction[4], scale * direction[5], 0, 0];
-};
-
-export const translationAlong = (line: Line, distance: number): Transform =>
- GA.add(GA.mul(GADirection.orthogonalToLine(line), 0.5 * distance), 1);
-
-export const compose = (motor1: Transform, motor2: Transform): Transform =>
- GA.mul(motor2, motor1);
-
-export const apply = (
- motor: Transform,
- nvector: Point | Direction | Line,
-): Point | Direction | Line =>
- GA.normalized(GA.mul(GA.mul(motor, nvector), GA.reverse(motor)));
diff --git a/packages/math/global.d.ts b/packages/math/global.d.ts
new file mode 100644
index 000000000..16ade7a7d
--- /dev/null
+++ b/packages/math/global.d.ts
@@ -0,0 +1,3 @@
+///
+import "@excalidraw/excalidraw/global";
+import "@excalidraw/excalidraw/css";
diff --git a/packages/math/line.ts b/packages/math/line.ts
deleted file mode 100644
index 89999baa9..000000000
--- a/packages/math/line.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { pointCenter, pointFrom, pointRotateRads } from "./point";
-import type { GlobalPoint, Line, LocalPoint, Radians } from "./types";
-
-/**
- * Create a line from two points.
- *
- * @param points The two points lying on the line
- * @returns The line on which the points lie
- */
-export function line(a: P, b: P): Line
{
- return [a, b] as Line
;
-}
-
-/**
- * Convenient point creation from an array of two points.
- *
- * @param param0 The array with the two points to convert to a line
- * @returns The created line
- */
-export function lineFromPointPair
([a, b]: [
- P,
- P,
-]): Line
{
- return line(a, b);
-}
-
-/**
- * TODO
- *
- * @param pointArray
- * @returns
- */
-export function lineFromPointArray
(
- pointArray: P[],
-): Line
| undefined {
- return pointArray.length === 2
- ? line
(pointArray[0], pointArray[1])
- : undefined;
-}
-
-/**
- * Return the coordinates resulting from rotating the given line about an
- * origin by an angle in degrees note that when the origin is not given,
- * the midpoint of the given line is used as the origin
- *
- * @param l
- * @param angle
- * @param origin
- * @returns
- */
-export const lineRotate = (
- l: Line,
- angle: Radians,
- origin?: Point,
-): Line => {
- return line(
- pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
- pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
- );
-};
-
-/**
- * Determines the intersection point (unless the lines are parallel) of two
- * lines
- *
- * @param a
- * @param b
- * @returns
- */
-export const linesIntersectAt = (
- a: Line,
- b: Line,
-): Point | null => {
- const A1 = a[1][1] - a[0][1];
- const B1 = a[0][0] - a[1][0];
- const A2 = b[1][1] - b[0][1];
- const B2 = b[0][0] - b[1][0];
- const D = A1 * B2 - A2 * B1;
- if (D !== 0) {
- const C1 = A1 * a[0][0] + B1 * a[0][1];
- const C2 = A2 * b[0][0] + B2 * b[0][1];
- return pointFrom((C1 * B2 - C2 * B1) / D, (A1 * C2 - A2 * C1) / D);
- }
-
- return null;
-};
diff --git a/packages/math/package.json b/packages/math/package.json
index b6c87e8f3..f8b411891 100644
--- a/packages/math/package.json
+++ b/packages/math/package.json
@@ -1,16 +1,21 @@
{
"name": "@excalidraw/math",
"version": "0.1.0",
- "main": "./dist/prod/index.js",
"type": "module",
+ "types": "./dist/types/math/src/index.d.ts",
+ "main": "./dist/prod/index.js",
"module": "./dist/prod/index.js",
"exports": {
".": {
+ "types": "./dist/types/math/src/index.d.ts",
"development": "./dist/dev/index.js",
+ "production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
+ },
+ "./*": {
+ "types": "./../math/dist/types/math/src/*.d.ts"
}
},
- "types": "./dist/utils/index.d.ts",
"files": [
"dist/*"
],
@@ -48,14 +53,8 @@
},
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw",
- "dependencies": {
- "@excalidraw/utils": "*"
- },
"scripts": {
"gen:types": "rm -rf types && tsc",
- "build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js",
- "build:esm": "rm -rf dist && node ../../scripts/buildUtils.js && yarn gen:types",
- "build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
- "pack": "yarn build:umd && yarn pack"
+ "build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
}
}
diff --git a/packages/math/angle.ts b/packages/math/src/angle.ts
similarity index 92%
rename from packages/math/angle.ts
rename to packages/math/src/angle.ts
index 2dc97a469..353dc5dad 100644
--- a/packages/math/angle.ts
+++ b/packages/math/src/angle.ts
@@ -1,3 +1,5 @@
+import { PRECISION } from "./utils";
+
import type {
Degrees,
GlobalPoint,
@@ -5,7 +7,6 @@ import type {
PolarCoords,
Radians,
} from "./types";
-import { PRECISION } from "./utils";
// TODO: Simplify with modulo and fix for angles beyond 4*Math.PI and - 4*Math.PI
export const normalizeRadians = (angle: Radians): Radians => {
@@ -26,7 +27,10 @@ export const normalizeRadians = (angle: Radians): Radians => {
export const cartesian2Polar = ([
x,
y,
-]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)];
+]: P): PolarCoords => [
+ Math.hypot(x, y),
+ normalizeRadians(Math.atan2(y, x) as Radians),
+];
export function degreesToRadians(degrees: Degrees): Radians {
return ((degrees * Math.PI) / 180) as Radians;
diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts
new file mode 100644
index 000000000..a79fb43a1
--- /dev/null
+++ b/packages/math/src/curve.ts
@@ -0,0 +1,284 @@
+import type { Bounds } from "@excalidraw/element/bounds";
+
+import { isPoint, pointDistance, pointFrom } from "./point";
+import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
+
+import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
+
+/**
+ *
+ * @param a
+ * @param b
+ * @param c
+ * @param d
+ * @returns
+ */
+export function curve(
+ a: Point,
+ b: Point,
+ c: Point,
+ d: Point,
+) {
+ return [a, b, c, d] as Curve;
+}
+
+function gradient(
+ f: (t: number, s: number) => number,
+ t0: number,
+ s0: number,
+ delta: number = 1e-6,
+): number[] {
+ return [
+ (f(t0 + delta, s0) - f(t0 - delta, s0)) / (2 * delta),
+ (f(t0, s0 + delta) - f(t0, s0 - delta)) / (2 * delta),
+ ];
+}
+
+function solve(
+ f: (t: number, s: number) => [number, number],
+ t0: number,
+ s0: number,
+ tolerance: number = 1e-3,
+ iterLimit: number = 10,
+): number[] | null {
+ let error = Infinity;
+ let iter = 0;
+
+ while (error >= tolerance) {
+ if (iter >= iterLimit) {
+ return null;
+ }
+
+ const y0 = f(t0, s0);
+ const jacobian = [
+ gradient((t, s) => f(t, s)[0], t0, s0),
+ gradient((t, s) => f(t, s)[1], t0, s0),
+ ];
+ const b = [[-y0[0]], [-y0[1]]];
+ const det =
+ jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
+
+ if (det === 0) {
+ return null;
+ }
+
+ const iJ = [
+ [jacobian[1][1] / det, -jacobian[0][1] / det],
+ [-jacobian[1][0] / det, jacobian[0][0] / det],
+ ];
+ const h = [
+ [iJ[0][0] * b[0][0] + iJ[0][1] * b[1][0]],
+ [iJ[1][0] * b[0][0] + iJ[1][1] * b[1][0]],
+ ];
+
+ t0 = t0 + h[0][0];
+ s0 = s0 + h[1][0];
+
+ const [tErr, sErr] = f(t0, s0);
+ error = Math.max(Math.abs(tErr), Math.abs(sErr));
+ iter += 1;
+ }
+
+ return [t0, s0];
+}
+
+const bezierEquation = (
+ c: Curve,
+ t: number,
+) =>
+ pointFrom(
+ (1 - t) ** 3 * c[0][0] +
+ 3 * (1 - t) ** 2 * t * c[1][0] +
+ 3 * (1 - t) * t ** 2 * c[2][0] +
+ t ** 3 * c[3][0],
+ (1 - t) ** 3 * c[0][1] +
+ 3 * (1 - t) ** 2 * t * c[1][1] +
+ 3 * (1 - t) * t ** 2 * c[2][1] +
+ t ** 3 * c[3][1],
+ );
+
+/**
+ * Computes the intersection between a cubic spline and a line segment.
+ */
+export function curveIntersectLineSegment<
+ Point extends GlobalPoint | LocalPoint,
+>(c: Curve, l: LineSegment): Point[] {
+ // Optimize by doing a cheap bounding box check first
+ const bounds = curveBounds(c);
+ if (
+ rectangleIntersectLineSegment(
+ rectangle(
+ pointFrom(bounds[0], bounds[1]),
+ pointFrom(bounds[2], bounds[3]),
+ ),
+ l,
+ ).length === 0
+ ) {
+ return [];
+ }
+
+ const line = (s: number) =>
+ pointFrom(
+ l[0][0] + s * (l[1][0] - l[0][0]),
+ l[0][1] + s * (l[1][1] - l[0][1]),
+ );
+
+ const initial_guesses: [number, number][] = [
+ [0.5, 0],
+ [0.2, 0],
+ [0.8, 0],
+ ];
+
+ const calculate = ([t0, s0]: [number, number]) => {
+ const solution = solve(
+ (t: number, s: number) => {
+ const bezier_point = bezierEquation(c, t);
+ const line_point = line(s);
+
+ return [
+ bezier_point[0] - line_point[0],
+ bezier_point[1] - line_point[1],
+ ];
+ },
+ t0,
+ s0,
+ );
+
+ if (!solution) {
+ return null;
+ }
+
+ const [t, s] = solution;
+
+ if (t < 0 || t > 1 || s < 0 || s > 1) {
+ return null;
+ }
+
+ return bezierEquation(c, t);
+ };
+
+ let solution = calculate(initial_guesses[0]);
+ if (solution) {
+ return [solution];
+ }
+
+ solution = calculate(initial_guesses[1]);
+ if (solution) {
+ return [solution];
+ }
+
+ solution = calculate(initial_guesses[2]);
+ if (solution) {
+ return [solution];
+ }
+
+ return [];
+}
+
+/**
+ * Finds the closest point on the Bezier curve from another point
+ *
+ * @param x
+ * @param y
+ * @param P0
+ * @param P1
+ * @param P2
+ * @param P3
+ * @param tolerance
+ * @param maxLevel
+ * @returns
+ */
+export function curveClosestPoint(
+ c: Curve,
+ p: Point,
+ tolerance: number = 1e-3,
+): Point | null {
+ const localMinimum = (
+ min: number,
+ max: number,
+ f: (t: number) => number,
+ e: number = tolerance,
+ ) => {
+ let m = min;
+ let n = max;
+ let k;
+
+ while (n - m > e) {
+ k = (n + m) / 2;
+ if (f(k - e) < f(k + e)) {
+ n = k;
+ } else {
+ m = k;
+ }
+ }
+
+ return k;
+ };
+
+ const maxSteps = 30;
+ let closestStep = 0;
+ for (let min = Infinity, step = 0; step < maxSteps; step++) {
+ const d = pointDistance(p, bezierEquation(c, step / maxSteps));
+ if (d < min) {
+ min = d;
+ closestStep = step;
+ }
+ }
+
+ const t0 = Math.max((closestStep - 1) / maxSteps, 0);
+ const t1 = Math.min((closestStep + 1) / maxSteps, 1);
+ const solution = localMinimum(t0, t1, (t) =>
+ pointDistance(p, bezierEquation(c, t)),
+ );
+
+ if (!solution) {
+ return null;
+ }
+
+ return bezierEquation(c, solution);
+}
+
+/**
+ * Determines the distance between a point and the closest point on the
+ * Bezier curve.
+ *
+ * @param c The curve to test
+ * @param p The point to measure from
+ */
+export function curvePointDistance(
+ c: Curve,
+ p: Point,
+) {
+ const closest = curveClosestPoint(c, p);
+
+ if (!closest) {
+ return 0;
+ }
+
+ return pointDistance(p, closest);
+}
+
+/**
+ * Determines if the parameter is a Curve
+ */
+export function isCurve(
+ v: unknown,
+): v is Curve
{
+ return (
+ Array.isArray(v) &&
+ v.length === 4 &&
+ isPoint(v[0]) &&
+ isPoint(v[1]) &&
+ isPoint(v[2]) &&
+ isPoint(v[3])
+ );
+}
+
+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)];
+}
diff --git a/packages/math/src/ellipse.ts b/packages/math/src/ellipse.ts
new file mode 100644
index 000000000..741a77df3
--- /dev/null
+++ b/packages/math/src/ellipse.ts
@@ -0,0 +1,231 @@
+import {
+ pointFrom,
+ pointDistance,
+ pointFromVector,
+ pointsEqual,
+} from "./point";
+import { PRECISION } from "./utils";
+import {
+ vector,
+ vectorAdd,
+ vectorDot,
+ vectorFromPoint,
+ vectorScale,
+} from "./vector";
+
+import type {
+ Ellipse,
+ GlobalPoint,
+ Line,
+ LineSegment,
+ LocalPoint,
+} from "./types";
+
+/**
+ * Construct an Ellipse object from the parameters
+ *
+ * @param center The center of the ellipse
+ * @param angle The slanting of the ellipse in radians
+ * @param halfWidth Half of the width of a non-slanted version of the ellipse
+ * @param halfHeight Half of the height of a non-slanted version of the ellipse
+ * @returns The constructed Ellipse object
+ */
+export function ellipse(
+ center: Point,
+ halfWidth: number,
+ halfHeight: number,
+): Ellipse {
+ return {
+ center,
+ halfWidth,
+ halfHeight,
+ } as Ellipse;
+}
+
+/**
+ * Determines if a point is inside or on the ellipse outline
+ *
+ * @param p The point to test
+ * @param ellipse The ellipse to compare against
+ * @returns TRUE if the point is inside or on the outline of the ellipse
+ */
+export const ellipseIncludesPoint = (
+ p: Point,
+ ellipse: Ellipse,
+) => {
+ const { center, halfWidth, halfHeight } = ellipse;
+ const normalizedX = (p[0] - center[0]) / halfWidth;
+ const normalizedY = (p[1] - center[1]) / halfHeight;
+
+ return normalizedX * normalizedX + normalizedY * normalizedY <= 1;
+};
+
+/**
+ * Tests whether a point lies on the outline of the ellipse within a given
+ * tolerance
+ *
+ * @param point The point to test
+ * @param ellipse The ellipse to compare against
+ * @param threshold The distance to consider a point close enough to be "on" the outline
+ * @returns TRUE if the point is on the ellise outline
+ */
+export const ellipseTouchesPoint = (
+ point: Point,
+ ellipse: Ellipse,
+ threshold = PRECISION,
+) => {
+ return ellipseDistanceFromPoint(point, ellipse) <= threshold;
+};
+
+/**
+ * Determine the shortest euclidean distance from a point to the
+ * outline of the ellipse
+ *
+ * @param p The point to consider
+ * @param ellipse The ellipse to calculate the distance to
+ * @returns The eucledian distance
+ */
+export const ellipseDistanceFromPoint = <
+ Point extends GlobalPoint | LocalPoint,
+>(
+ p: Point,
+ ellipse: Ellipse,
+): number => {
+ const { halfWidth, halfHeight, center } = ellipse;
+ const a = halfWidth;
+ const b = halfHeight;
+ const translatedPoint = vectorAdd(
+ vectorFromPoint(p),
+ vectorScale(vectorFromPoint(center), -1),
+ );
+
+ const px = Math.abs(translatedPoint[0]);
+ const py = Math.abs(translatedPoint[1]);
+
+ let tx = 0.707;
+ let ty = 0.707;
+
+ for (let i = 0; i < 3; i++) {
+ const x = a * tx;
+ const y = b * ty;
+
+ const ex = ((a * a - b * b) * tx ** 3) / a;
+ const ey = ((b * b - a * a) * ty ** 3) / b;
+
+ const rx = x - ex;
+ const ry = y - ey;
+
+ const qx = px - ex;
+ const qy = py - ey;
+
+ const r = Math.hypot(ry, rx);
+ const q = Math.hypot(qy, qx);
+
+ tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
+ ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
+ const t = Math.hypot(ty, tx);
+ tx /= t;
+ ty /= t;
+ }
+
+ const [minX, minY] = [
+ a * tx * Math.sign(translatedPoint[0]),
+ b * ty * Math.sign(translatedPoint[1]),
+ ];
+
+ return pointDistance(pointFromVector(translatedPoint), pointFrom(minX, minY));
+};
+
+/**
+ * Calculate a maximum of two intercept points for a line going throug an
+ * ellipse.
+ */
+export function ellipseSegmentInterceptPoints<
+ Point extends GlobalPoint | LocalPoint,
+>(e: Readonly>, s: Readonly>): Point[] {
+ const rx = e.halfWidth;
+ const ry = e.halfHeight;
+
+ const dir = vectorFromPoint(s[1], s[0]);
+ const diff = vector(s[0][0] - e.center[0], s[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));
+
+ const a = vectorDot(dir, mDir);
+ const b = vectorDot(dir, mDiff);
+ const c = vectorDot(diff, mDiff) - 1.0;
+ const d = b * b - a * c;
+
+ const intersections: Point[] = [];
+
+ if (d > 0) {
+ const t_a = (-b - Math.sqrt(d)) / a;
+ const t_b = (-b + Math.sqrt(d)) / a;
+
+ if (0 <= t_a && t_a <= 1) {
+ intersections.push(
+ pointFrom(
+ s[0][0] + (s[1][0] - s[0][0]) * t_a,
+ s[0][1] + (s[1][1] - s[0][1]) * t_a,
+ ),
+ );
+ }
+
+ if (0 <= t_b && t_b <= 1) {
+ intersections.push(
+ pointFrom(
+ s[0][0] + (s[1][0] - s[0][0]) * t_b,
+ s[0][1] + (s[1][1] - s[0][1]) * t_b,
+ ),
+ );
+ }
+ } else if (d === 0) {
+ const t = -b / a;
+ if (0 <= t && t <= 1) {
+ intersections.push(
+ pointFrom(
+ s[0][0] + (s[1][0] - s[0][0]) * t,
+ s[0][1] + (s[1][1] - s[0][1]) * t,
+ ),
+ );
+ }
+ }
+
+ return intersections;
+}
+
+export function ellipseLineIntersectionPoints<
+ Point extends GlobalPoint | LocalPoint,
+>(
+ { center, halfWidth, halfHeight }: Ellipse,
+ [g, h]: Line,
+): Point[] {
+ const [cx, cy] = center;
+ const x1 = g[0] - cx;
+ const y1 = g[1] - cy;
+ const x2 = h[0] - cx;
+ const y2 = h[1] - cy;
+ const a =
+ Math.pow(x2 - x1, 2) / Math.pow(halfWidth, 2) +
+ Math.pow(y2 - y1, 2) / Math.pow(halfHeight, 2);
+ const b =
+ 2 *
+ ((x1 * (x2 - x1)) / Math.pow(halfWidth, 2) +
+ (y1 * (y2 - y1)) / Math.pow(halfHeight, 2));
+ const c =
+ Math.pow(x1, 2) / Math.pow(halfWidth, 2) +
+ Math.pow(y1, 2) / Math.pow(halfHeight, 2) -
+ 1;
+ const t1 = (-b + Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a);
+ const t2 = (-b - Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a);
+ const candidates = [
+ pointFrom(x1 + t1 * (x2 - x1) + cx, y1 + t1 * (y2 - y1) + cy),
+ pointFrom(x1 + t2 * (x2 - x1) + cx, y1 + t2 * (y2 - y1) + cy),
+ ].filter((p) => !isNaN(p[0]) && !isNaN(p[1]));
+
+ if (candidates.length === 2 && pointsEqual(candidates[0], candidates[1])) {
+ return [candidates[0]];
+ }
+
+ return candidates;
+}
diff --git a/packages/math/index.ts b/packages/math/src/index.ts
similarity index 90%
rename from packages/math/index.ts
rename to packages/math/src/index.ts
index 05ec5158f..d00ab469d 100644
--- a/packages/math/index.ts
+++ b/packages/math/src/index.ts
@@ -1,10 +1,10 @@
-export * from "./arc";
export * from "./angle";
export * from "./curve";
export * from "./line";
export * from "./point";
export * from "./polygon";
export * from "./range";
+export * from "./rectangle";
export * from "./segment";
export * from "./triangle";
export * from "./types";
diff --git a/packages/math/src/line.ts b/packages/math/src/line.ts
new file mode 100644
index 000000000..889fa08ce
--- /dev/null
+++ b/packages/math/src/line.ts
@@ -0,0 +1,39 @@
+import { pointFrom } from "./point";
+
+import type { GlobalPoint, Line, LocalPoint } from "./types";
+
+/**
+ * Create a line from two points.
+ *
+ * @param points The two points lying on the line
+ * @returns The line on which the points lie
+ */
+export function line(a: P, b: P): Line
{
+ return [a, b] as Line
;
+}
+
+/**
+ * Determines the intersection point (unless the lines are parallel) of two
+ * lines
+ *
+ * @param a
+ * @param b
+ * @returns
+ */
+export function linesIntersectAt(
+ a: Line,
+ b: Line,
+): Point | null {
+ const A1 = a[1][1] - a[0][1];
+ const B1 = a[0][0] - a[1][0];
+ const A2 = b[1][1] - b[0][1];
+ const B2 = b[0][0] - b[1][0];
+ const D = A1 * B2 - A2 * B1;
+ if (D !== 0) {
+ const C1 = A1 * a[0][0] + B1 * a[0][1];
+ const C2 = A2 * b[0][0] + B2 * b[0][1];
+ return pointFrom((C1 * B2 - C2 * B1) / D, (A1 * C2 - A2 * C1) / D);
+ }
+
+ return null;
+}
diff --git a/packages/math/point.ts b/packages/math/src/point.ts
similarity index 84%
rename from packages/math/point.ts
rename to packages/math/src/point.ts
index 40f178efb..b6054a10a 100644
--- a/packages/math/point.ts
+++ b/packages/math/src/point.ts
@@ -1,4 +1,7 @@
import { degreesToRadians } from "./angle";
+import { PRECISION } from "./utils";
+import { vectorFromPoint, vectorScale } from "./vector";
+
import type {
LocalPoint,
GlobalPoint,
@@ -6,8 +9,6 @@ import type {
Degrees,
Vector,
} from "./types";
-import { PRECISION } from "./utils";
-import { vectorFromPoint, vectorScale } from "./vector";
/**
* Create a properly typed Point instance from the X and Y coordinates.
@@ -57,24 +58,9 @@ export function pointFromPair(
*/
export function pointFromVector(
v: Vector,
+ offset: P = pointFrom(0, 0),
): P {
- return v as unknown as P;
-}
-
-/**
- * Convert the coordiante object to a point.
- *
- * @param coords The coordinate object with x and y properties
- * @returns
- */
-export function pointFromCoords({
- x,
- y,
-}: {
- x: number;
- y: number;
-}) {
- return [x, y] as Point;
+ return pointFrom(offset[0] + v[0], offset[1] + v[1]);
}
/**
@@ -111,7 +97,7 @@ export function pointsEqual(
}
/**
- * Roate a point by [angle] radians.
+ * Rotate a point by [angle] radians.
*
* @param point The point to rotate
* @param center The point to rotate around, the center point
@@ -130,7 +116,7 @@ export function pointRotateRads(
}
/**
- * Roate a point by [angle] degree.
+ * Rotate a point by [angle] degree.
*
* @param point The point to rotate
* @param center The point to rotate around, the center point
@@ -176,36 +162,6 @@ export function pointCenter(a: P, b: P): P {
return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
}
-/**
- * Add together two points by their coordinates like you'd apply a translation
- * to a point by a vector.
- *
- * @param a One point to act as a basis
- * @param b The other point to act like the vector to translate by
- * @returns
- */
-export function pointAdd(
- a: Point,
- b: Point,
-): Point {
- return pointFrom(a[0] + b[0], a[1] + b[1]);
-}
-
-/**
- * Subtract a point from another point like you'd translate a point by an
- * invese vector.
- *
- * @param a The point to translate
- * @param b The point which will act like a vector
- * @returns The resulting point
- */
-export function pointSubtract(
- a: Point,
- b: Point,
-): Point {
- return pointFrom(a[0] - b[0], a[1] - b[1]);
-}
-
/**
* Calculate the distance between two points.
*
diff --git a/packages/math/polygon.ts b/packages/math/src/polygon.ts
similarity index 99%
rename from packages/math/polygon.ts
rename to packages/math/src/polygon.ts
index 783bc4cf3..762c82dbf 100644
--- a/packages/math/polygon.ts
+++ b/packages/math/src/polygon.ts
@@ -1,8 +1,9 @@
import { pointsEqual } from "./point";
import { lineSegment, pointOnLineSegment } from "./segment";
-import type { GlobalPoint, LocalPoint, Polygon } from "./types";
import { PRECISION } from "./utils";
+import type { GlobalPoint, LocalPoint, Polygon } from "./types";
+
export function polygon(
...points: Point[]
) {
diff --git a/packages/math/range.ts b/packages/math/src/range.ts
similarity index 97%
rename from packages/math/range.ts
rename to packages/math/src/range.ts
index 314d1c8ae..1b292344e 100644
--- a/packages/math/range.ts
+++ b/packages/math/src/range.ts
@@ -1,4 +1,5 @@
-import { toBrandedType } from "../excalidraw/utils";
+import { toBrandedType } from "@excalidraw/common";
+
import type { InclusiveRange } from "./types";
/**
diff --git a/packages/math/src/rectangle.ts b/packages/math/src/rectangle.ts
new file mode 100644
index 000000000..394b5c2f8
--- /dev/null
+++ b/packages/math/src/rectangle.ts
@@ -0,0 +1,24 @@
+import { pointFrom } from "./point";
+import { lineSegment, lineSegmentIntersectionPoints } from "./segment";
+
+import type { GlobalPoint, LineSegment, LocalPoint, Rectangle } from "./types";
+
+export function rectangle(
+ topLeft: P,
+ bottomRight: P,
+): Rectangle
{
+ return [topLeft, bottomRight] as Rectangle
;
+}
+
+export function rectangleIntersectLineSegment<
+ Point extends LocalPoint | GlobalPoint,
+>(r: Rectangle, l: LineSegment): Point[] {
+ return [
+ lineSegment(r[0], pointFrom(r[1][0], r[0][1])),
+ lineSegment(pointFrom(r[1][0], r[0][1]), r[1]),
+ lineSegment(r[1], pointFrom(r[0][0], r[1][1])),
+ lineSegment(pointFrom(r[0][0], r[1][1]), r[0]),
+ ]
+ .map((s) => lineSegmentIntersectionPoints(l, s))
+ .filter((i): i is Point => !!i);
+}
diff --git a/packages/math/segment.ts b/packages/math/src/segment.ts
similarity index 86%
rename from packages/math/segment.ts
rename to packages/math/src/segment.ts
index 6c0c2de34..e38978b7e 100644
--- a/packages/math/segment.ts
+++ b/packages/math/src/segment.ts
@@ -1,10 +1,10 @@
+import { line, linesIntersectAt } from "./line";
import {
isPoint,
pointCenter,
pointFromVector,
pointRotateRads,
} from "./point";
-import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
import { PRECISION } from "./utils";
import {
vectorAdd,
@@ -14,6 +14,8 @@ import {
vectorSubtract,
} from "./vector";
+import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
+
/**
* Create a line segment from two points.
*
@@ -27,14 +29,6 @@ export function lineSegment(
return [a, b] as LineSegment
;
}
-export function lineSegmentFromPointArray
(
- pointArray: P[],
-): LineSegment
| undefined {
- return pointArray.length === 2
- ? lineSegment
(pointArray[0], pointArray[1])
- : undefined;
-}
-
/**
*
* @param segment
@@ -156,3 +150,26 @@ export const distanceToLineSegment = (
const dy = y - yy;
return Math.sqrt(dx * dx + dy * dy);
};
+
+/**
+ * Returns the intersection point of a segment and a line
+ *
+ * @param l
+ * @param s
+ * @returns
+ */
+export function lineSegmentIntersectionPoints<
+ Point extends GlobalPoint | LocalPoint,
+>(l: LineSegment, s: LineSegment): Point | null {
+ const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
+
+ if (
+ !candidate ||
+ !pointOnLineSegment(candidate, s) ||
+ !pointOnLineSegment(candidate, l)
+ ) {
+ return null;
+ }
+
+ return candidate;
+}
diff --git a/packages/math/triangle.ts b/packages/math/src/triangle.ts
similarity index 100%
rename from packages/math/triangle.ts
rename to packages/math/src/triangle.ts
diff --git a/packages/math/types.ts b/packages/math/src/types.ts
similarity index 80%
rename from packages/math/types.ts
rename to packages/math/src/types.ts
index 138a44bc0..a2a575bd7 100644
--- a/packages/math/types.ts
+++ b/packages/math/src/types.ts
@@ -85,6 +85,13 @@ export type Triangle = [
_brand: "excalimath__triangle";
};
+/**
+ * A rectangular shape represented by 4 points at its corners
+ */
+export type Rectangle
= [a: P, b: P] & {
+ _brand: "excalimath__rectangle";
+};
+
//
// Polygon
//
@@ -120,11 +127,14 @@ export type PolarCoords = [
];
/**
- * 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".
+ An ellipse is specified by its center, angle, and its major and minor axes
+ but for the sake of simplicity, we've used halfWidth and halfHeight instead
+ in replace of semi major and semi minor axes
*/
-export type SymmetricArc = {
- radius: number;
- startAngle: number;
- endAngle: number;
+export type Ellipse = {
+ center: Point;
+ halfWidth: number;
+ halfHeight: number;
+} & {
+ _brand: "excalimath_ellipse";
};
diff --git a/packages/math/utils.ts b/packages/math/src/utils.ts
similarity index 100%
rename from packages/math/utils.ts
rename to packages/math/src/utils.ts
diff --git a/packages/math/vector.ts b/packages/math/src/vector.ts
similarity index 95%
rename from packages/math/vector.ts
rename to packages/math/src/vector.ts
index d7d51b14e..246722067 100644
--- a/packages/math/vector.ts
+++ b/packages/math/src/vector.ts
@@ -137,12 +137,9 @@ export function vectorMagnitude(v: Vector) {
export const vectorNormalize = (v: Vector): Vector => {
const m = vectorMagnitude(v);
+ if (m === 0) {
+ return vector(0, 0);
+ }
+
return vector(v[0] / m, v[1] / m);
};
-
-/**
- * Project the first vector onto the second vector
- */
-export const vectorProjection = (a: Vector, b: Vector) => {
- return vectorScale(b, vectorDot(a, b) / vectorDot(b, b));
-};
diff --git a/packages/math/tests/curve.test.ts b/packages/math/tests/curve.test.ts
new file mode 100644
index 000000000..739562096
--- /dev/null
+++ b/packages/math/tests/curve.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/packages/math/tests/ellipse.test.ts b/packages/math/tests/ellipse.test.ts
new file mode 100644
index 000000000..4fa0d4e59
--- /dev/null
+++ b/packages/math/tests/ellipse.test.ts
@@ -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 = 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 = 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(pointFrom(-100, 0), pointFrom(-10, 0)),
+ ),
+ ).toEqual([]);
+ expect(
+ ellipseSegmentInterceptPoints(
+ e,
+ lineSegment(pointFrom(-10, 0), pointFrom(10, 0)),
+ ),
+ ).toEqual([pointFrom(-2, 0), pointFrom(2, 0)]);
+ expect(
+ ellipseSegmentInterceptPoints(
+ e,
+ lineSegment(pointFrom(-10, -2), pointFrom(10, -2)),
+ ),
+ ).toEqual([pointFrom(0, -2)]);
+ expect(
+ ellipseSegmentInterceptPoints(
+ e,
+ lineSegment(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(pointFrom(-10, -10), pointFrom(10, -10)),
+ ),
+ ).toEqual([]);
+ });
+ it("detects line intersecting ellipse", () => {
+ expect(
+ ellipseLineIntersectionPoints(
+ e,
+ line(pointFrom(0, -1), pointFrom(0, 1)),
+ ),
+ ).toEqual([pointFrom(0, 2), pointFrom(0, -2)]);
+ expect(
+ ellipseLineIntersectionPoints(
+ e,
+ line