chore: Unify math types, utils and functions (#8389)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács 2024-09-03 00:23:38 +02:00 committed by GitHub
parent e3d1dee9d0
commit f4dd23fc31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
98 changed files with 4291 additions and 3661 deletions

View file

@ -1,249 +1,122 @@
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
import {
lineIntersectsLine,
lineRotate,
pointInEllipse,
pointInPolygon,
pointLeftofLine,
pointOnCurve,
pointOnEllipse,
pointOnLine,
point,
lineSegment,
polygon,
pointOnLineSegment,
pointOnPolygon,
pointOnPolyline,
pointRightofLine,
pointRotate,
} from "./geometry";
import type { Curve, Ellipse, Line, Point, Polygon, Polyline } from "./shape";
polygonIncludesPoint,
segmentsIntersectAt,
} from "../../math";
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
describe("point and line", () => {
const line: Line = [
[1, 0],
[1, 2],
];
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
it("point on left or right of line", () => {
expect(pointLeftofLine([0, 1], line)).toBe(true);
expect(pointLeftofLine([1, 1], line)).toBe(false);
expect(pointLeftofLine([2, 1], line)).toBe(false);
// it("point on left or right of line", () => {
// expect(pointLeftofLine(point(0, 1), l)).toBe(true);
// expect(pointLeftofLine(point(1, 1), l)).toBe(false);
// expect(pointLeftofLine(point(2, 1), l)).toBe(false);
expect(pointRightofLine([0, 1], line)).toBe(false);
expect(pointRightofLine([1, 1], line)).toBe(false);
expect(pointRightofLine([2, 1], line)).toBe(true);
});
// expect(pointRightofLine(point(0, 1), l)).toBe(false);
// expect(pointRightofLine(point(1, 1), l)).toBe(false);
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
// });
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
it("point on the line", () => {
expect(pointOnLine([0, 1], line)).toBe(false);
expect(pointOnLine([1, 1], line, 0)).toBe(true);
expect(pointOnLine([2, 1], line)).toBe(false);
});
});
describe("point and polylines", () => {
const polyline: Polyline = [
[
[1, 0],
[1, 2],
],
[
[1, 2],
[2, 2],
],
[
[2, 2],
[2, 1],
],
[
[2, 1],
[3, 1],
],
];
it("point on the line", () => {
expect(pointOnPolyline([1, 0], polyline)).toBe(true);
expect(pointOnPolyline([1, 2], polyline)).toBe(true);
expect(pointOnPolyline([2, 2], polyline)).toBe(true);
expect(pointOnPolyline([2, 1], polyline)).toBe(true);
expect(pointOnPolyline([3, 1], polyline)).toBe(true);
expect(pointOnPolyline([1, 1], polyline)).toBe(true);
expect(pointOnPolyline([2, 1.5], polyline)).toBe(true);
expect(pointOnPolyline([2.5, 1], polyline)).toBe(true);
expect(pointOnPolyline([0, 1], polyline)).toBe(false);
expect(pointOnPolyline([2.1, 1.5], polyline)).toBe(false);
});
it("point on the line with rotation", () => {
const truePoints = [
[1, 0],
[1, 2],
[2, 2],
[2, 1],
[3, 1],
] as Point[];
truePoints.forEach((point) => {
const rotation = Math.random() * 360;
const rotatedPoint = pointRotate(point, rotation);
const rotatedPolyline: Polyline = polyline.map((line) =>
lineRotate(line, rotation, [0, 0]),
);
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
});
const falsePoints = [
[0, 1],
[2.1, 1.5],
] as Point[];
falsePoints.forEach((point) => {
const rotation = Math.random() * 360;
const rotatedPoint = pointRotate(point, rotation);
const rotatedPolyline: Polyline = polyline.map((line) =>
lineRotate(line, rotation, [0, 0]),
);
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
});
expect(pointOnLineSegment(point(0, 1), s)).toBe(false);
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true);
expect(pointOnLineSegment(point(2, 1), s)).toBe(false);
});
});
describe("point and polygon", () => {
const polygon: Polygon = [
[10, 10],
[50, 10],
[50, 50],
[10, 50],
];
const poly: Polygon<GlobalPoint> = polygon(
point(10, 10),
point(50, 10),
point(50, 50),
point(10, 50),
);
it("point on polygon", () => {
expect(pointOnPolygon([30, 10], polygon)).toBe(true);
expect(pointOnPolygon([50, 30], polygon)).toBe(true);
expect(pointOnPolygon([30, 50], polygon)).toBe(true);
expect(pointOnPolygon([10, 30], polygon)).toBe(true);
expect(pointOnPolygon([30, 30], polygon)).toBe(false);
expect(pointOnPolygon([30, 70], polygon)).toBe(false);
expect(pointOnPolygon(point(30, 10), poly)).toBe(true);
expect(pointOnPolygon(point(50, 30), poly)).toBe(true);
expect(pointOnPolygon(point(30, 50), poly)).toBe(true);
expect(pointOnPolygon(point(10, 30), poly)).toBe(true);
expect(pointOnPolygon(point(30, 30), poly)).toBe(false);
expect(pointOnPolygon(point(30, 70), poly)).toBe(false);
});
it("point in polygon", () => {
const polygon: Polygon = [
[0, 0],
[2, 0],
[2, 2],
[0, 2],
];
expect(pointInPolygon([1, 1], polygon)).toBe(true);
expect(pointInPolygon([3, 3], polygon)).toBe(false);
});
});
describe("point and curve", () => {
const curve: Curve = [
[1.4, 1.65],
[1.9, 7.9],
[5.9, 1.65],
[6.44, 4.84],
];
it("point on curve", () => {
expect(pointOnCurve(curve[0], curve)).toBe(true);
expect(pointOnCurve(curve[3], curve)).toBe(true);
expect(pointOnCurve([2, 4], curve, 0.1)).toBe(true);
expect(pointOnCurve([4, 4.4], curve, 0.1)).toBe(true);
expect(pointOnCurve([5.6, 3.85], curve, 0.1)).toBe(true);
expect(pointOnCurve([5.6, 4], curve, 0.1)).toBe(false);
expect(pointOnCurve(curve[1], curve, 0.1)).toBe(false);
expect(pointOnCurve(curve[2], curve, 0.1)).toBe(false);
const poly: Polygon<GlobalPoint> = polygon(
point(0, 0),
point(2, 0),
point(2, 2),
point(0, 2),
);
expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true);
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
});
});
describe("point and ellipse", () => {
const ellipse: Ellipse = {
center: [0, 0],
angle: 0,
const ellipse: Ellipse<GlobalPoint> = {
center: point(0, 0),
angle: 0 as Radians,
halfWidth: 2,
halfHeight: 1,
};
it("point on ellipse", () => {
[
[0, 1],
[0, -1],
[2, 0],
[-2, 0],
].forEach((point) => {
expect(pointOnEllipse(point as Point, ellipse)).toBe(true);
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
expect(pointOnEllipse(p, ellipse)).toBe(true);
});
expect(pointOnEllipse([-1.4, 0.7], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([-1.4, 0.71], ellipse, 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([1.4, 0.7], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([1.4, 0.71], ellipse, 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([1, -0.86], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([1, -0.86], ellipse, 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([-1, -0.86], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([-1, -0.86], ellipse, 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([-1, 0.8], ellipse)).toBe(false);
expect(pointOnEllipse([1, -0.8], ellipse)).toBe(false);
expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false);
expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false);
});
it("point in ellipse", () => {
[
[0, 1],
[0, -1],
[2, 0],
[-2, 0],
].forEach((point) => {
expect(pointInEllipse(point as Point, ellipse)).toBe(true);
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
expect(pointInEllipse(p, ellipse)).toBe(true);
});
expect(pointInEllipse([-1, 0.8], ellipse)).toBe(true);
expect(pointInEllipse([1, -0.8], ellipse)).toBe(true);
expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true);
expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true);
expect(pointInEllipse([-1, 1], ellipse)).toBe(false);
expect(pointInEllipse([-1.4, 0.8], ellipse)).toBe(false);
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
});
});
describe("line and line", () => {
const lineA: Line = [
[1, 4],
[3, 4],
];
const lineB: Line = [
[2, 1],
[2, 7],
];
const lineC: Line = [
[1, 8],
[3, 8],
];
const lineD: Line = [
[1, 8],
[3, 8],
];
const lineE: Line = [
[1, 9],
[3, 9],
];
const lineF: Line = [
[1, 2],
[3, 4],
];
const lineG: Line = [
[0, 1],
[2, 3],
];
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
it("intersection", () => {
expect(lineIntersectsLine(lineA, lineB)).toBe(true);
expect(lineIntersectsLine(lineA, lineC)).toBe(false);
expect(lineIntersectsLine(lineB, lineC)).toBe(false);
expect(lineIntersectsLine(lineC, lineD)).toBe(true);
expect(lineIntersectsLine(lineE, lineD)).toBe(false);
expect(lineIntersectsLine(lineF, lineG)).toBe(true);
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
});
});

File diff suppressed because it is too large Load diff

View file

@ -12,9 +12,30 @@
* to pure shapes
*/
import type { Curve, LineSegment, Polygon, Radians } from "../../math";
import {
curve,
lineSegment,
point,
pointDistance,
pointFromArray,
pointFromVector,
pointRotateRads,
polygon,
polygonFromPoints,
PRECISION,
segmentsIntersectAt,
vector,
vectorAdd,
vectorFromPoint,
vectorScale,
type GlobalPoint,
type LocalPoint,
} from "../../math";
import { getElementAbsoluteCoords } from "../../excalidraw/element";
import type {
ElementsMap,
ExcalidrawBindableElement,
ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawEllipseElement,
@ -28,67 +49,54 @@ import type {
ExcalidrawSelectionElement,
ExcalidrawTextElement,
} from "../../excalidraw/element/types";
import { angleToDegrees, close, pointAdd, pointRotate } from "./geometry";
import { pointsOnBezierCurves } from "points-on-curve";
import type { Drawable, Op } from "roughjs/bin/core";
// a point is specified by its coordinate (x, y)
export type Point = [number, number];
export type Vector = Point;
// a line (segment) is defined by two endpoints
export type Line = [Point, Point];
import { invariant } from "../../excalidraw/utils";
// a polyline (made up term here) is a line consisting of other line segments
// this corresponds to a straight line element in the editor but it could also
// be used to model other elements
export type Polyline = Line[];
// cubic bezier curve with four control points
export type Curve = [Point, Point, Point, Point];
export type Polyline<Point extends GlobalPoint | LocalPoint> =
LineSegment<Point>[];
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
// curve on the canvas
export type Polycurve = Curve[];
// a polygon is a closed shape by connecting the given points
// rectangles and diamonds are modelled by polygons
export type Polygon = Point[];
export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
// 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 Ellipse = {
export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
center: Point;
angle: number;
angle: Radians;
halfWidth: number;
halfHeight: number;
};
export type GeometricShape =
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
| {
type: "line";
data: Line;
data: LineSegment<Point>;
}
| {
type: "polygon";
data: Polygon;
data: Polygon<Point>;
}
| {
type: "curve";
data: Curve;
data: Curve<Point>;
}
| {
type: "ellipse";
data: Ellipse;
data: Ellipse<Point>;
}
| {
type: "polyline";
data: Polyline;
data: Polyline<Point>;
}
| {
type: "polycurve";
data: Polycurve;
data: Polycurve<Point>;
};
type RectangularElement =
@ -102,32 +110,32 @@ type RectangularElement =
| ExcalidrawSelectionElement;
// polygon
export const getPolygonShape = (
export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
element: RectangularElement,
): GeometricShape => {
): GeometricShape<Point> => {
const { angle, width, height, x, y } = element;
const angleInDegrees = angleToDegrees(angle);
const cx = x + width / 2;
const cy = y + height / 2;
const center: Point = [cx, cy];
const center: Point = point(cx, cy);
let data: Polygon = [];
let data: Polygon<Point>;
if (element.type === "diamond") {
data = [
pointRotate([cx, y], angleInDegrees, center),
pointRotate([x + width, cy], angleInDegrees, center),
pointRotate([cx, y + height], angleInDegrees, center),
pointRotate([x, cy], angleInDegrees, center),
] as Polygon;
data = polygon(
pointRotateRads(point(cx, y), center, angle),
pointRotateRads(point(x + width, cy), center, angle),
pointRotateRads(point(cx, y + height), center, angle),
pointRotateRads(point(x, cy), center, angle),
);
} else {
data = [
pointRotate([x, y], angleInDegrees, center),
pointRotate([x + width, y], angleInDegrees, center),
pointRotate([x + width, y + height], angleInDegrees, center),
pointRotate([x, y + height], angleInDegrees, center),
] as Polygon;
data = polygon(
pointRotateRads(point(x, y), center, angle),
pointRotateRads(point(x + width, y), center, angle),
pointRotateRads(point(x + width, y + height), center, angle),
pointRotateRads(point(x, y + height), center, angle),
);
}
return {
@ -137,7 +145,7 @@ export const getPolygonShape = (
};
// return the selection box for an element, possibly rotated as well
export const getSelectionBoxShape = (
export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawElement,
elementsMap: ElementsMap,
padding = 10,
@ -153,29 +161,29 @@ export const getSelectionBoxShape = (
y1 -= padding;
y2 += padding;
const angleInDegrees = angleToDegrees(element.angle);
const center: Point = [cx, cy];
const topLeft = pointRotate([x1, y1], angleInDegrees, center);
const topRight = pointRotate([x2, y1], angleInDegrees, center);
const bottomLeft = pointRotate([x1, y2], angleInDegrees, center);
const bottomRight = pointRotate([x2, y2], angleInDegrees, center);
//const angleInDegrees = angleToDegrees(element.angle);
const center = point(cx, cy);
const topLeft = pointRotateRads(point(x1, y1), center, element.angle);
const topRight = pointRotateRads(point(x2, y1), center, element.angle);
const bottomLeft = pointRotateRads(point(x1, y2), center, element.angle);
const bottomRight = pointRotateRads(point(x2, y2), center, element.angle);
return {
type: "polygon",
data: [topLeft, topRight, bottomRight, bottomLeft],
} as GeometricShape;
} as GeometricShape<Point>;
};
// ellipse
export const getEllipseShape = (
export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawEllipseElement,
): GeometricShape => {
): GeometricShape<Point> => {
const { width, height, angle, x, y } = element;
return {
type: "ellipse",
data: {
center: [x + width / 2, y + height / 2],
center: point(x + width / 2, y + height / 2),
angle,
halfWidth: width / 2,
halfHeight: height / 2,
@ -193,32 +201,34 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
};
// linear
export const getCurveShape = (
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
roughShape: Drawable,
startingPoint: Point = [0, 0],
angleInRadian: number,
startingPoint: Point = point(0, 0),
angleInRadian: Radians,
center: Point,
): GeometricShape => {
const transform = (p: Point) =>
pointRotate(
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
angleToDegrees(angleInRadian),
): GeometricShape<Point> => {
const transform = (p: Point): Point =>
pointRotateRads(
point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
center,
angleInRadian,
);
const ops = getCurvePathOps(roughShape);
const polycurve: Polycurve = [];
let p0: Point = [0, 0];
const polycurve: Polycurve<Point> = [];
let p0 = point<Point>(0, 0);
for (const op of ops) {
if (op.op === "move") {
p0 = transform(op.data as Point);
const p = pointFromArray<Point>(op.data);
invariant(p != null, "Ops data is not a point");
p0 = transform(p);
}
if (op.op === "bcurveTo") {
const p1: Point = transform([op.data[0], op.data[1]]);
const p2: Point = transform([op.data[2], op.data[3]]);
const p3: Point = transform([op.data[4], op.data[5]]);
polycurve.push([p0, p1, p2, p3]);
const p1 = transform(point<Point>(op.data[0], op.data[1]));
const p2 = transform(point<Point>(op.data[2], op.data[3]));
const p3 = transform(point<Point>(op.data[4], op.data[5]));
polycurve.push(curve<Point>(p0, p1, p2, p3));
p0 = p3;
}
}
@ -229,61 +239,72 @@ export const getCurveShape = (
};
};
const polylineFromPoints = (points: Point[]) => {
let previousPoint = points[0];
const polyline: Polyline = [];
const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
points: Point[],
): Polyline<Point> => {
let previousPoint: Point = points[0];
const polyline: LineSegment<Point>[] = [];
for (let i = 1; i < points.length; i++) {
const nextPoint = points[i];
polyline.push([previousPoint, nextPoint]);
polyline.push(lineSegment<Point>(previousPoint, nextPoint));
previousPoint = nextPoint;
}
return polyline;
};
export const getFreedrawShape = (
export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawFreeDrawElement,
center: Point,
isClosed: boolean = false,
): GeometricShape => {
const angle = angleToDegrees(element.angle);
): GeometricShape<Point> => {
const transform = (p: Point) =>
pointRotate(pointAdd(p, [element.x, element.y] as Point), angle, center);
pointRotateRads(
pointFromVector(
vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
),
center,
element.angle,
);
const polyline = polylineFromPoints(
element.points.map((p) => transform(p as Point)),
);
return isClosed
? {
type: "polygon",
data: close(polyline.flat()) as Polygon,
}
: {
type: "polyline",
data: polyline,
};
return (
isClosed
? {
type: "polygon",
data: polygonFromPoints(polyline.flat()),
}
: {
type: "polyline",
data: polyline,
}
) as GeometricShape<Point>;
};
export const getClosedCurveShape = (
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawLinearElement,
roughShape: Drawable,
startingPoint: Point = [0, 0],
angleInRadian: number,
startingPoint: Point = point<Point>(0, 0),
angleInRadian: Radians,
center: Point,
): GeometricShape => {
): GeometricShape<Point> => {
const transform = (p: Point) =>
pointRotate(
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
angleToDegrees(angleInRadian),
pointRotateRads(
point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
center,
angleInRadian,
);
if (element.roundness === null) {
return {
type: "polygon",
data: close(element.points.map((p) => transform(p as Point))),
data: polygonFromPoints(
element.points.map((p) => transform(p as Point)) as Point[],
),
};
}
@ -295,27 +316,218 @@ export const getClosedCurveShape = (
if (operation.op === "move") {
odd = !odd;
if (odd) {
points.push([operation.data[0], operation.data[1]]);
points.push(point(operation.data[0], operation.data[1]));
}
} else if (operation.op === "bcurveTo") {
if (odd) {
points.push([operation.data[0], operation.data[1]]);
points.push([operation.data[2], operation.data[3]]);
points.push([operation.data[4], operation.data[5]]);
points.push(point(operation.data[0], operation.data[1]));
points.push(point(operation.data[2], operation.data[3]));
points.push(point(operation.data[4], operation.data[5]));
}
} else if (operation.op === "lineTo") {
if (odd) {
points.push([operation.data[0], operation.data[1]]);
points.push(point(operation.data[0], operation.data[1]));
}
}
}
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
transform(p),
);
transform(p as Point),
) as Point[];
return {
type: "polygon",
data: polygonPoints,
data: polygonFromPoints<Point>(polygonPoints),
};
};
/**
* Determine intersection of a rectangular shaped element and a
* line segment.
*
* @param element The rectangular element to test against
* @param segment The segment intersecting the element
* @param gap Optional value to inflate the shape before testing
* @returns An array of intersections
*/
// TODO: Replace with final rounded rectangle code
export const segmentIntersectRectangleElement = <
Point extends LocalPoint | GlobalPoint,
>(
element: ExcalidrawBindableElement,
segment: LineSegment<Point>,
gap: number = 0,
): Point[] => {
const bounds = [
element.x - gap,
element.y - gap,
element.x + element.width + gap,
element.y + element.height + gap,
];
const center = point(
(bounds[0] + bounds[2]) / 2,
(bounds[1] + bounds[3]) / 2,
);
return [
lineSegment(
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
),
lineSegment(
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
),
lineSegment(
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
),
lineSegment(
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
),
]
.map((s) => segmentsIntersectAt(segment, s))
.filter((i): i is Point => !!i);
};
const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
p: Point,
ellipse: Ellipse<Point>,
) => {
const { angle, halfWidth, halfHeight, center } = ellipse;
const a = halfWidth;
const b = halfHeight;
const translatedPoint = vectorAdd(
vectorFromPoint(p),
vectorScale(vectorFromPoint(center), -1),
);
const [rotatedPointX, rotatedPointY] = pointRotateRads(
pointFromVector(translatedPoint),
point(0, 0),
-angle as Radians,
);
const px = Math.abs(rotatedPointX);
const py = Math.abs(rotatedPointY);
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(rotatedPointX),
b * ty * Math.sign(rotatedPointY),
];
return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY));
};
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
point: Point,
ellipse: Ellipse<Point>,
threshold = PRECISION,
) => {
return distanceToEllipse(point, ellipse) <= threshold;
};
export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
p: Point,
ellipse: Ellipse<Point>,
) => {
const { center, angle, halfWidth, halfHeight } = ellipse;
const translatedPoint = vectorAdd(
vectorFromPoint(p),
vectorScale(vectorFromPoint(center), -1),
);
const [rotatedPointX, rotatedPointY] = pointRotateRads(
pointFromVector(translatedPoint),
point(0, 0),
-angle as Radians,
);
return (
(rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
(rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
1
);
};
export const ellipseAxes = <Point extends LocalPoint | GlobalPoint>(
ellipse: Ellipse<Point>,
) => {
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 {
majorAxis,
minorAxis,
};
};
export const ellipseFocusToCenter = <Point extends LocalPoint | GlobalPoint>(
ellipse: Ellipse<Point>,
) => {
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
};
export const ellipseExtremes = <Point extends LocalPoint | GlobalPoint>(
ellipse: Ellipse<Point>,
) => {
const { center, angle } = ellipse;
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
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),
];
};