mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Add ellipse changes no angle
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
d9ea7190ec
commit
7b4e989d65
12 changed files with 97 additions and 357 deletions
|
@ -1,5 +1,5 @@
|
|||
import { radians } from "./angle";
|
||||
import { arc, arcIncludesPoint, arcSegmentInterceptPoint } from "./arc";
|
||||
import { arc, arcIncludesPoint, arcSegmentInterceptPoints } from "./arc";
|
||||
import { point } from "./point";
|
||||
import { segment } from "./segment";
|
||||
|
||||
|
@ -33,7 +33,7 @@ describe("point on arc", () => {
|
|||
describe("intersection", () => {
|
||||
it("should report correct interception point", () => {
|
||||
expect(
|
||||
arcSegmentInterceptPoint(
|
||||
arcSegmentInterceptPoints(
|
||||
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
|
||||
segment(point(2, 1), point(0, 0)),
|
||||
),
|
||||
|
@ -42,7 +42,7 @@ describe("intersection", () => {
|
|||
|
||||
it("should report both interception points when present", () => {
|
||||
expect(
|
||||
arcSegmentInterceptPoint(
|
||||
arcSegmentInterceptPoints(
|
||||
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
|
||||
segment(point(0.9, -2), point(0.9, 2)),
|
||||
),
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { invariant } from "../excalidraw/utils";
|
||||
import { cartesian2Polar, radians } from "./angle";
|
||||
import { ellipse, ellipseSegmentInterceptPoints } from "./ellipse";
|
||||
import { cartesian2Polar, normalizeRadians, radians } from "./angle";
|
||||
import {
|
||||
ellipse,
|
||||
ellipseDistanceFromPoint,
|
||||
ellipseSegmentInterceptPoints,
|
||||
} from "./ellipse";
|
||||
import { point, pointDistance } from "./point";
|
||||
import { segment } from "./segment";
|
||||
import type { GenericPoint, Segment, Radians, Arc } from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
|
||||
|
@ -53,26 +55,44 @@ export function arcDistanceFromPoint<Point extends GenericPoint>(
|
|||
a: Arc<Point>,
|
||||
p: Point,
|
||||
) {
|
||||
const intersectPoint = arcSegmentInterceptPoint(a, segment(p, a.center));
|
||||
|
||||
invariant(
|
||||
intersectPoint.length !== 1,
|
||||
"Arc distance intersector cannot have multiple intersections",
|
||||
const theta = normalizeRadians(
|
||||
radians(Math.atan2(p[0] - a.center[0], p[1] - a.center[1])),
|
||||
);
|
||||
|
||||
return pointDistance(intersectPoint[0], p);
|
||||
if (a.startAngle <= theta && a.endAngle >= theta) {
|
||||
return ellipseDistanceFromPoint(
|
||||
p,
|
||||
ellipse(a.center, 2 * a.radius, 2 * a.radius),
|
||||
);
|
||||
}
|
||||
return Math.min(
|
||||
pointDistance(
|
||||
p,
|
||||
point(
|
||||
a.center[0] + a.radius + Math.cos(a.startAngle),
|
||||
a.center[1] + a.radius + Math.sin(a.startAngle),
|
||||
),
|
||||
),
|
||||
pointDistance(
|
||||
p,
|
||||
point(
|
||||
a.center[0] + a.radius + Math.cos(a.endAngle),
|
||||
a.center[1] + a.radius + Math.sin(a.endAngle),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intersection point(s) of a line segment represented by a start
|
||||
* point and end point and a symmetric arc.
|
||||
*/
|
||||
export function arcSegmentInterceptPoint<Point extends GenericPoint>(
|
||||
export function arcSegmentInterceptPoints<Point extends GenericPoint>(
|
||||
a: Readonly<Arc<Point>>,
|
||||
s: Readonly<Segment<Point>>,
|
||||
): Point[] {
|
||||
return ellipseSegmentInterceptPoints(
|
||||
ellipse(a.center, radians(0), a.radius, a.radius),
|
||||
ellipse(a.center, a.radius, a.radius),
|
||||
s,
|
||||
).filter((candidate) => {
|
||||
const [candidateRadius, candidateAngle] = cartesian2Polar(
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { radians } from "./angle";
|
||||
import {
|
||||
ellipse,
|
||||
ellipseSegmentInterceptPoints,
|
||||
|
@ -10,29 +9,29 @@ import { segment } from "./segment";
|
|||
import type { Ellipse, GlobalPoint } from "./types";
|
||||
|
||||
describe("point and ellipse", () => {
|
||||
const target: Ellipse<GlobalPoint> = ellipse(point(0, 0), radians(0), 2, 1);
|
||||
|
||||
it("point on ellipse", () => {
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
const target: Ellipse<GlobalPoint> = ellipse(point(1, 2), 2, 1);
|
||||
[point(1, 3), point(1, 1), point(3, 2), point(-1, 2)].forEach((p) => {
|
||||
expect(ellipseTouchesPoint(p, target)).toBe(true);
|
||||
});
|
||||
expect(ellipseTouchesPoint(point(-1.4, 0.7), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(-1.4, 0.71), target, 0.01)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(-0.4, 2.7), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(-0.4, 2.71), target, 0.01)).toBe(true);
|
||||
|
||||
expect(ellipseTouchesPoint(point(1.4, 0.7), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(1.4, 0.71), target, 0.01)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(2.4, 2.7), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(2.4, 2.71), target, 0.01)).toBe(true);
|
||||
|
||||
expect(ellipseTouchesPoint(point(1, -0.86), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(1, -0.86), target, 0.01)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(2, 1.14), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(2, 1.14), target, 0.01)).toBe(true);
|
||||
|
||||
expect(ellipseTouchesPoint(point(-1, -0.86), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(-1, -0.86), target, 0.01)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(0, 1.14), target, 0.1)).toBe(true);
|
||||
expect(ellipseTouchesPoint(point(0, 1.14), target, 0.01)).toBe(true);
|
||||
|
||||
expect(ellipseTouchesPoint(point(-1, 0.8), target)).toBe(false);
|
||||
expect(ellipseTouchesPoint(point(1, -0.8), target)).toBe(false);
|
||||
expect(ellipseTouchesPoint(point(0, 2.8), target)).toBe(false);
|
||||
expect(ellipseTouchesPoint(point(2, 1.2), target)).toBe(false);
|
||||
});
|
||||
|
||||
it("point in ellipse", () => {
|
||||
const target: Ellipse<GlobalPoint> = ellipse(point(0, 0), 2, 1);
|
||||
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
|
||||
expect(ellipseIncludesPoint(p, target)).toBe(true);
|
||||
});
|
||||
|
@ -50,7 +49,7 @@ describe("point and ellipse", () => {
|
|||
|
||||
describe("line and ellipse", () => {
|
||||
it("detects outside segment", () => {
|
||||
const e = ellipse(point(0, 0), radians(0), 2, 2);
|
||||
const e = ellipse(point(0, 0), 2, 2);
|
||||
|
||||
expect(
|
||||
ellipseSegmentInterceptPoints(
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import { radians } from "./angle";
|
||||
import { line } from "./line";
|
||||
import {
|
||||
point,
|
||||
pointDistance,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
} from "./point";
|
||||
import type { Ellipse, GenericPoint, Segment, Radians } from "./types";
|
||||
import { point, pointDistance, pointFromVector } from "./point";
|
||||
import type { Ellipse, GenericPoint, Segment } from "./types";
|
||||
import { PRECISION } from "./utils";
|
||||
import {
|
||||
vector,
|
||||
|
@ -27,13 +20,11 @@ import {
|
|||
*/
|
||||
export function ellipse<Point extends GenericPoint>(
|
||||
center: Point,
|
||||
angle: Radians,
|
||||
halfWidth: number,
|
||||
halfHeight: number,
|
||||
): Ellipse<Point> {
|
||||
return {
|
||||
center,
|
||||
angle,
|
||||
halfWidth,
|
||||
halfHeight,
|
||||
} as Ellipse<Point>;
|
||||
|
@ -50,22 +41,11 @@ export const ellipseIncludesPoint = <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),
|
||||
);
|
||||
const { center, halfWidth, halfHeight } = ellipse;
|
||||
const normalizedX = (p[0] - center[0]) / halfWidth;
|
||||
const normalizedY = (p[1] - center[1]) / halfHeight;
|
||||
|
||||
return (
|
||||
(rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
|
||||
(rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
|
||||
1
|
||||
);
|
||||
return normalizedX * normalizedX + normalizedY * normalizedY <= 1;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -97,21 +77,16 @@ export const ellipseDistanceFromPoint = <Point extends GenericPoint>(
|
|||
p: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
): number => {
|
||||
const { angle, halfWidth, halfHeight, center } = ellipse;
|
||||
const { 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);
|
||||
const px = Math.abs(translatedPoint[0]);
|
||||
const py = Math.abs(translatedPoint[1]);
|
||||
|
||||
let tx = 0.707;
|
||||
let ty = 0.707;
|
||||
|
@ -140,11 +115,11 @@ export const ellipseDistanceFromPoint = <Point extends GenericPoint>(
|
|||
}
|
||||
|
||||
const [minX, minY] = [
|
||||
a * tx * Math.sign(rotatedPointX),
|
||||
b * ty * Math.sign(rotatedPointY),
|
||||
a * tx * Math.sign(translatedPoint[0]),
|
||||
b * ty * Math.sign(translatedPoint[1]),
|
||||
];
|
||||
|
||||
return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY));
|
||||
return pointDistance(pointFromVector(translatedPoint), point(minX, minY));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -153,19 +128,13 @@ export const ellipseDistanceFromPoint = <Point extends GenericPoint>(
|
|||
*/
|
||||
export function ellipseSegmentInterceptPoints<Point extends GenericPoint>(
|
||||
e: Readonly<Ellipse<Point>>,
|
||||
l: Readonly<Segment<Point>>,
|
||||
s: Readonly<Segment<Point>>,
|
||||
): Point[] {
|
||||
const rx = e.halfWidth;
|
||||
const ry = e.halfHeight;
|
||||
const nonRotatedLine = line(
|
||||
pointRotateRads(l[0], e.center, radians(-e.angle)),
|
||||
pointRotateRads(l[1], e.center, radians(-e.angle)),
|
||||
);
|
||||
const dir = vectorFromPoint(nonRotatedLine[1], nonRotatedLine[0]);
|
||||
const diff = vector(
|
||||
nonRotatedLine[0][0] - e.center[0],
|
||||
nonRotatedLine[0][1] - e.center[1],
|
||||
);
|
||||
|
||||
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));
|
||||
|
||||
|
@ -183,10 +152,8 @@ export function ellipseSegmentInterceptPoints<Point extends GenericPoint>(
|
|||
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,
|
||||
s[0][0] + (s[1][0] - s[0][0]) * t_a,
|
||||
s[0][1] + (s[1][1] - s[0][1]) * t_a,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -194,10 +161,8 @@ export function ellipseSegmentInterceptPoints<Point extends GenericPoint>(
|
|||
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,
|
||||
s[0][0] + (s[1][0] - s[0][0]) * t_b,
|
||||
s[0][1] + (s[1][1] - s[0][1]) * t_b,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -206,16 +171,12 @@ export function ellipseSegmentInterceptPoints<Point extends GenericPoint>(
|
|||
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,
|
||||
s[0][0] + (s[1][0] - s[0][0]) * t,
|
||||
s[0][1] + (s[1][1] - s[0][1]) * t,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return intersections.map((point) =>
|
||||
pointRotateRads(point, e.center, e.angle),
|
||||
);
|
||||
return intersections;
|
||||
}
|
||||
|
|
|
@ -137,7 +137,6 @@ export type Extent = {
|
|||
// in replace of semi major and semi minor axes
|
||||
export type Ellipse<Point extends GenericPoint> = {
|
||||
center: Point;
|
||||
angle: Radians;
|
||||
halfWidth: number;
|
||||
halfHeight: number;
|
||||
} & {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue