mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Ellipse refactor
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
017047d15e
commit
3efa8735ef
10 changed files with 301 additions and 217 deletions
|
@ -1,4 +1,4 @@
|
|||
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
|
||||
import type { GlobalPoint, LineSegment, Polygon } from "../../math";
|
||||
import {
|
||||
point,
|
||||
lineSegment,
|
||||
|
@ -7,7 +7,6 @@ import {
|
|||
pointOnPolygon,
|
||||
polygonIncludesPoint,
|
||||
} from "../../math";
|
||||
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
|
||||
|
||||
describe("point and line", () => {
|
||||
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
||||
|
@ -47,44 +46,3 @@ describe("point and polygon", () => {
|
|||
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and ellipse", () => {
|
||||
const ellipse: Ellipse<GlobalPoint> = {
|
||||
center: point(0, 0),
|
||||
angle: 0 as Radians,
|
||||
halfWidth: 2,
|
||||
halfHeight: 1,
|
||||
};
|
||||
|
||||
it("point on ellipse", () => {
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(1.4, 0.71), 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(point(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
|
||||
expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false);
|
||||
expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in ellipse", () => {
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
expect(pointInEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
|
||||
expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true);
|
||||
expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true);
|
||||
|
||||
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false);
|
||||
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
import type {
|
||||
Curve,
|
||||
Ellipse,
|
||||
GenericPoint,
|
||||
LineSegment,
|
||||
Polygon,
|
||||
Radians,
|
||||
|
@ -23,18 +25,15 @@ import {
|
|||
curve,
|
||||
lineSegment,
|
||||
point,
|
||||
pointDistance,
|
||||
pointFromArray,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
polygon,
|
||||
polygonFromPoints,
|
||||
PRECISION,
|
||||
segmentsIntersectAt,
|
||||
vector,
|
||||
vectorAdd,
|
||||
vectorFromPoint,
|
||||
vectorScale,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
} from "../../math";
|
||||
|
@ -70,19 +69,7 @@ export type Polyline<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
|||
export type Polycurve<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
||||
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<Point extends GlobalPoint | LocalPoint | ViewportPoint> = {
|
||||
center: Point;
|
||||
angle: Radians;
|
||||
halfWidth: number;
|
||||
halfHeight: number;
|
||||
};
|
||||
|
||||
export type GeometricShape<
|
||||
Point extends GlobalPoint | LocalPoint | ViewportPoint,
|
||||
> =
|
||||
export type GeometricShape<Point extends GenericPoint> =
|
||||
| {
|
||||
type: "line";
|
||||
data: LineSegment<Point>;
|
||||
|
@ -403,150 +390,3 @@ export const segmentIntersectRectangleElement = <
|
|||
.map((s) => segmentsIntersectAt(segment, s))
|
||||
.filter((i): i is Point => !!i);
|
||||
};
|
||||
|
||||
const distanceToEllipse = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
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 | ViewportPoint,
|
||||
>(
|
||||
point: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
threshold = PRECISION,
|
||||
) => {
|
||||
return distanceToEllipse(point, ellipse) <= threshold;
|
||||
};
|
||||
|
||||
export const pointInEllipse = <
|
||||
Point extends LocalPoint | GlobalPoint | ViewportPoint,
|
||||
>(
|
||||
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),
|
||||
];
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue