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,5 +1,5 @@
|
||||||
import type { Radians } from "../math";
|
import type { Radians } from "../math";
|
||||||
import { point } from "../math";
|
import { point, radians } from "../math";
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
DEFAULT_CHART_COLOR_INDEX,
|
DEFAULT_CHART_COLOR_INDEX,
|
||||||
|
@ -205,7 +205,7 @@ const chartXLabels = (
|
||||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||||
y: y + BAR_GAP / 2,
|
y: y + BAR_GAP / 2,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
angle: 5.87 as Radians,
|
angle: radians(5.87),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
|
|
|
@ -55,8 +55,8 @@ import {
|
||||||
getNormalizedGridStep,
|
getNormalizedGridStep,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { pointExtent, isFiniteNumber, point } from "../../math";
|
import { pointExtent, isFiniteNumber, point, radians } from "../../math";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -153,7 +153,7 @@ const restoreElementWithProperties = <
|
||||||
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness,
|
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness,
|
||||||
opacity:
|
opacity:
|
||||||
element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity,
|
element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity,
|
||||||
angle: element.angle || (0 as Radians),
|
angle: element.angle || radians(0),
|
||||||
x: extra.x ?? element.x ?? 0,
|
x: extra.x ?? element.x ?? 0,
|
||||||
y: extra.y ?? element.y ?? 0,
|
y: extra.y ?? element.y ?? 0,
|
||||||
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor,
|
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
|
|
|
@ -73,6 +73,7 @@ import {
|
||||||
pointFromPair,
|
pointFromPair,
|
||||||
pointDistanceSq,
|
pointDistanceSq,
|
||||||
clamp,
|
clamp,
|
||||||
|
radians,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
||||||
|
|
||||||
|
@ -845,7 +846,7 @@ export const avoidRectangularCorner = (
|
||||||
element.x + element.width / 2,
|
element.x + element.width / 2,
|
||||||
element.y + element.height / 2,
|
element.y + element.height / 2,
|
||||||
);
|
);
|
||||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const nonRotatedPoint = pointRotateRads(p, center, radians(-element.angle));
|
||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
// Top left
|
// Top left
|
||||||
|
|
45
packages/math/ellipse.test.ts
Normal file
45
packages/math/ellipse.test.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { radians } from "./angle";
|
||||||
|
import { pointInEllipse, pointOnEllipse } from "./ellipse";
|
||||||
|
import { point } from "./point";
|
||||||
|
import type { Ellipse, GlobalPoint } from "./types";
|
||||||
|
|
||||||
|
describe("point and ellipse", () => {
|
||||||
|
const ellipse: Ellipse<GlobalPoint> = {
|
||||||
|
center: point(0, 0),
|
||||||
|
angle: radians(0),
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
231
packages/math/ellipse.ts
Normal file
231
packages/math/ellipse.ts
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
import { radians } from "./angle";
|
||||||
|
import { line } from "./line";
|
||||||
|
import {
|
||||||
|
point,
|
||||||
|
pointDistance,
|
||||||
|
pointFromVector,
|
||||||
|
pointRotateRads,
|
||||||
|
} from "./point";
|
||||||
|
import type { Ellipse, GenericPoint, Line } from "./types";
|
||||||
|
import { PRECISION } from "./utils";
|
||||||
|
import {
|
||||||
|
vector,
|
||||||
|
vectorAdd,
|
||||||
|
vectorDot,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorScale,
|
||||||
|
} from "./vector";
|
||||||
|
|
||||||
|
export const pointInEllipse = <Point extends GenericPoint>(
|
||||||
|
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),
|
||||||
|
radians(-angle),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
(rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
|
||||||
|
(rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
|
||||||
|
1
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointOnEllipse = <Point extends GenericPoint>(
|
||||||
|
point: Point,
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
threshold = PRECISION,
|
||||||
|
) => {
|
||||||
|
return distanceToEllipse(point, ellipse) <= threshold;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ellipseAxes = <Point extends GenericPoint>(
|
||||||
|
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 GenericPoint>(
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
) => {
|
||||||
|
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
|
||||||
|
|
||||||
|
return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ellipseExtremes = <Point extends GenericPoint>(
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const distanceToEllipse = <Point extends GenericPoint>(
|
||||||
|
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),
|
||||||
|
radians(-angle),
|
||||||
|
);
|
||||||
|
|
||||||
|
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));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a maximum of two intercept points for a line going throug an
|
||||||
|
* ellipse.
|
||||||
|
*/
|
||||||
|
export function interceptPointsOfLineAndEllipse<Point extends GenericPoint>(
|
||||||
|
ellipse: Readonly<Ellipse<Point>>,
|
||||||
|
l: Readonly<Line<Point>>,
|
||||||
|
): Point[] {
|
||||||
|
const rx = ellipse.halfWidth;
|
||||||
|
const ry = ellipse.halfHeight;
|
||||||
|
const nonRotatedLine = line(
|
||||||
|
pointRotateRads(l[0], ellipse.center, radians(-ellipse.angle)),
|
||||||
|
pointRotateRads(l[1], ellipse.center, radians(-ellipse.angle)),
|
||||||
|
);
|
||||||
|
const dir = vectorFromPoint(nonRotatedLine[1], nonRotatedLine[0]);
|
||||||
|
const diff = vector(
|
||||||
|
nonRotatedLine[0][0] - ellipse.center[0],
|
||||||
|
nonRotatedLine[0][1] - ellipse.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(
|
||||||
|
point(
|
||||||
|
nonRotatedLine[0][0] +
|
||||||
|
(nonRotatedLine[1][0] - nonRotatedLine[0][0]) * t_a,
|
||||||
|
nonRotatedLine[0][1] +
|
||||||
|
(nonRotatedLine[1][1] - nonRotatedLine[0][1]) * t_a,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 <= t_b && t_b <= 1) {
|
||||||
|
intersections.push(
|
||||||
|
point(
|
||||||
|
nonRotatedLine[0][0] +
|
||||||
|
(nonRotatedLine[1][0] - nonRotatedLine[0][0]) * t_b,
|
||||||
|
nonRotatedLine[0][1] +
|
||||||
|
(nonRotatedLine[1][1] - nonRotatedLine[0][1]) * t_b,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (d === 0) {
|
||||||
|
const t = -b / a;
|
||||||
|
if (0 <= t && t <= 1) {
|
||||||
|
intersections.push(
|
||||||
|
point(
|
||||||
|
nonRotatedLine[0][0] +
|
||||||
|
(nonRotatedLine[1][0] - nonRotatedLine[0][0]) * t,
|
||||||
|
nonRotatedLine[0][1] +
|
||||||
|
(nonRotatedLine[1][1] - nonRotatedLine[0][1]) * t,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections.map((point) =>
|
||||||
|
pointRotateRads(point, ellipse.center, ellipse.angle),
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
export * from "./arc";
|
export * from "./arc";
|
||||||
export * from "./angle";
|
export * from "./angle";
|
||||||
export * from "./curve";
|
export * from "./curve";
|
||||||
|
export * from "./ellipse";
|
||||||
export * from "./line";
|
export * from "./line";
|
||||||
export * from "./path";
|
export * from "./path";
|
||||||
export * from "./point";
|
export * from "./point";
|
||||||
|
|
|
@ -143,3 +143,13 @@ export type Extent = {
|
||||||
} & {
|
} & {
|
||||||
_brand: "excalimath_extent";
|
_brand: "excalimath_extent";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 GenericPoint> = {
|
||||||
|
center: Point;
|
||||||
|
angle: Radians;
|
||||||
|
halfWidth: number;
|
||||||
|
halfHeight: number;
|
||||||
|
};
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
import type { Polycurve, Polyline } from "./geometry/shape";
|
import type { Polycurve, Polyline } from "./geometry/shape";
|
||||||
import {
|
import { type GeometricShape } from "./geometry/shape";
|
||||||
pointInEllipse,
|
|
||||||
pointOnEllipse,
|
|
||||||
type GeometricShape,
|
|
||||||
} from "./geometry/shape";
|
|
||||||
import type { Curve, ViewportPoint } from "../math";
|
import type { Curve, ViewportPoint } from "../math";
|
||||||
import {
|
import {
|
||||||
lineSegment,
|
lineSegment,
|
||||||
|
@ -15,6 +11,8 @@ import {
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
type Polygon,
|
type Polygon,
|
||||||
|
pointOnEllipse,
|
||||||
|
pointInEllipse,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
|
|
||||||
// check if the given point is considered on the given shape's border
|
// check if the given point is considered on the given shape's border
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
|
import type { GlobalPoint, LineSegment, Polygon } from "../../math";
|
||||||
import {
|
import {
|
||||||
point,
|
point,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
|
@ -7,7 +7,6 @@ import {
|
||||||
pointOnPolygon,
|
pointOnPolygon,
|
||||||
polygonIncludesPoint,
|
polygonIncludesPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { pointInEllipse, pointOnEllipse, type Ellipse } from "./shape";
|
|
||||||
|
|
||||||
describe("point and line", () => {
|
describe("point and line", () => {
|
||||||
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2));
|
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);
|
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 {
|
import type {
|
||||||
Curve,
|
Curve,
|
||||||
|
Ellipse,
|
||||||
|
GenericPoint,
|
||||||
LineSegment,
|
LineSegment,
|
||||||
Polygon,
|
Polygon,
|
||||||
Radians,
|
Radians,
|
||||||
|
@ -23,18 +25,15 @@ import {
|
||||||
curve,
|
curve,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
point,
|
point,
|
||||||
pointDistance,
|
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointFromVector,
|
pointFromVector,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
polygon,
|
polygon,
|
||||||
polygonFromPoints,
|
polygonFromPoints,
|
||||||
PRECISION,
|
|
||||||
segmentsIntersectAt,
|
segmentsIntersectAt,
|
||||||
vector,
|
vector,
|
||||||
vectorAdd,
|
vectorAdd,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
vectorScale,
|
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
|
@ -70,19 +69,7 @@ export type Polyline<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
||||||
export type Polycurve<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
export type Polycurve<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
|
||||||
Curve<Point>[];
|
Curve<Point>[];
|
||||||
|
|
||||||
// an ellipse is specified by its center, angle, and its major and minor axes
|
export type GeometricShape<Point extends GenericPoint> =
|
||||||
// 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,
|
|
||||||
> =
|
|
||||||
| {
|
| {
|
||||||
type: "line";
|
type: "line";
|
||||||
data: LineSegment<Point>;
|
data: LineSegment<Point>;
|
||||||
|
@ -403,150 +390,3 @@ export const segmentIntersectRectangleElement = <
|
||||||
.map((s) => segmentsIntersectAt(segment, s))
|
.map((s) => segmentsIntersectAt(segment, s))
|
||||||
.filter((i): i is Point => !!i);
|
.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