mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Arc tests
This commit is contained in:
parent
f79fb899fc
commit
3fe73e79a6
9 changed files with 80 additions and 76 deletions
|
@ -12,11 +12,7 @@ import {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "../../packages/excalidraw/components/icons";
|
} from "../../packages/excalidraw/components/icons";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
import {
|
import { isSegment, type GlobalPoint, type Segment } from "../../packages/math";
|
||||||
isSegment,
|
|
||||||
type GlobalPoint,
|
|
||||||
type Segment,
|
|
||||||
} from "../../packages/math";
|
|
||||||
|
|
||||||
const renderLine = (
|
const renderLine = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { Radians } from "../math";
|
|
||||||
import { point, radians } from "../math";
|
import { point, radians } from "../math";
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
|
|
|
@ -136,12 +136,7 @@ const getElementLineSegments = (
|
||||||
).map((point) => pointRotateRads(point, center, element.angle));
|
).map((point) => pointRotateRads(point, center, element.angle));
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
if (element.type === "diamond") {
|
||||||
return [
|
return [segment(n, w), segment(n, e), segment(s, w), segment(s, e)];
|
||||||
segment(n, w),
|
|
||||||
segment(n, e),
|
|
||||||
segment(s, w),
|
|
||||||
segment(s, e),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.type === "ellipse") {
|
if (element.type === "ellipse") {
|
||||||
|
|
13
packages/math/angle.test.ts
Normal file
13
packages/math/angle.test.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { cartesian2Polar, polar, radians } from "./angle";
|
||||||
|
import { point } from "./point";
|
||||||
|
|
||||||
|
describe("cartesian to polar coordinate conversion", () => {
|
||||||
|
it("converts values properly", () => {
|
||||||
|
expect(cartesian2Polar(point(12, 5))).toEqual(
|
||||||
|
polar(13, radians(Math.atan(5 / 12))),
|
||||||
|
);
|
||||||
|
expect(cartesian2Polar(point(5, 5))).toEqual(
|
||||||
|
polar(5 * Math.sqrt(2), radians(Math.PI / 4)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import { radians } from "./angle";
|
import { radians } from "./angle";
|
||||||
import { arc, arcIncludesPoint } from "./arc";
|
import { arc, arcIncludesPoint, arcSegmentInterceptPoint } from "./arc";
|
||||||
import { point } from "./point";
|
import { point } from "./point";
|
||||||
|
import { segment } from "./segment";
|
||||||
|
|
||||||
describe("point on arc", () => {
|
describe("point on arc", () => {
|
||||||
it("should detect point on simple arc", () => {
|
it("should detect point on simple arc", () => {
|
||||||
|
@ -28,3 +29,26 @@ describe("point on arc", () => {
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("intersection", () => {
|
||||||
|
it("should report correct interception point", () => {
|
||||||
|
expect(
|
||||||
|
arcSegmentInterceptPoint(
|
||||||
|
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
|
||||||
|
segment(point(2, 1), point(0, 0)),
|
||||||
|
),
|
||||||
|
).toEqual([point(0.894427190999916, 0.447213595499958)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should report both interception points when present", () => {
|
||||||
|
expect(
|
||||||
|
arcSegmentInterceptPoint(
|
||||||
|
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
|
||||||
|
segment(point(0.9, -2), point(0.9, 2)),
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
point(0.9, -0.4358898943540668),
|
||||||
|
point(0.9, 0.4358898943540668),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -6,11 +6,11 @@ import { PRECISION } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a symmetric arc defined by the originating circle radius
|
* Constructs a symmetric arc defined by the originating circle radius
|
||||||
* the start angle and end angle with 0 radians being the "northest" point
|
* the start angle and end angle with 0 radians being the most "eastward" point
|
||||||
* of the circle.
|
* of the circle.
|
||||||
*
|
*
|
||||||
* @param radius The radius of the circle this arc lies on
|
* @param radius The radius of the circle this arc lies on
|
||||||
* @param startAngle The start angle with 0 radians being the "northest" point
|
* @param startAngle The start angle with 0 radians being the most "eastward" point
|
||||||
* @param endAngle The end angle with 0 radians being the "northest" point
|
* @param endAngle The end angle with 0 radians being the "northest" point
|
||||||
* @returns The constructed symmetric arc
|
* @returns The constructed symmetric arc
|
||||||
*/
|
*/
|
||||||
|
@ -46,20 +46,20 @@ export function arcIncludesPoint<P extends GenericPoint>(
|
||||||
* Returns the intersection point(s) of a line segment represented by a start
|
* Returns the intersection point(s) of a line segment represented by a start
|
||||||
* point and end point and a symmetric arc.
|
* point and end point and a symmetric arc.
|
||||||
*/
|
*/
|
||||||
export function interceptOfSymmetricArcAndSegment<Point extends GenericPoint>(
|
export function arcSegmentInterceptPoint<Point extends GenericPoint>(
|
||||||
a: Readonly<Arc<Point>>,
|
a: Readonly<Arc<Point>>,
|
||||||
l: Readonly<Segment<Point>>,
|
s: Readonly<Segment<Point>>,
|
||||||
): Point[] {
|
): Point[] {
|
||||||
return ellipseSegmentInterceptPoints(
|
return ellipseSegmentInterceptPoints(
|
||||||
ellipse(a.center, radians(0), a.radius, a.radius),
|
ellipse(a.center, radians(0), a.radius, a.radius),
|
||||||
l,
|
s,
|
||||||
).filter((candidate) => {
|
).filter((candidate) => {
|
||||||
const [candidateRadius, candidateAngle] = cartesian2Polar(
|
const [candidateRadius, candidateAngle] = cartesian2Polar(
|
||||||
point(candidate[0] - a.center[0], candidate[1] - a.center[1]),
|
point(candidate[0] - a.center[0], candidate[1] - a.center[1]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return a.startAngle < a.endAngle
|
return a.startAngle < a.endAngle
|
||||||
? Math.abs(a.radius - candidateRadius) < 0.0000001 &&
|
? Math.abs(a.radius - candidateRadius) < PRECISION &&
|
||||||
a.startAngle <= candidateAngle &&
|
a.startAngle <= candidateAngle &&
|
||||||
a.endAngle >= candidateAngle
|
a.endAngle >= candidateAngle
|
||||||
: a.startAngle <= candidateAngle || a.endAngle >= candidateAngle;
|
: a.startAngle <= candidateAngle || a.endAngle >= candidateAngle;
|
||||||
|
|
|
@ -50,8 +50,31 @@ describe("point and ellipse", () => {
|
||||||
|
|
||||||
describe("line and ellipse", () => {
|
describe("line and ellipse", () => {
|
||||||
it("detects outside segment", () => {
|
it("detects outside segment", () => {
|
||||||
const l = segment<GlobalPoint>(point(-100, 0), point(-10, 0));
|
|
||||||
const e = ellipse(point(0, 0), radians(0), 2, 2);
|
const e = ellipse(point(0, 0), radians(0), 2, 2);
|
||||||
expect(ellipseSegmentInterceptPoints(e, l).length).toBe(0);
|
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
segment<GlobalPoint>(point(-100, 0), point(-10, 0)),
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
segment<GlobalPoint>(point(-10, 0), point(10, 0)),
|
||||||
|
),
|
||||||
|
).toEqual([point(-2, 0), point(2, 0)]);
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
segment<GlobalPoint>(point(-10, -2), point(10, -2)),
|
||||||
|
),
|
||||||
|
).toEqual([point(0, -2)]);
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
segment<GlobalPoint>(point(0, -1), point(0, 1)),
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -82,63 +82,10 @@ export const ellipseTouchesPoint = <Point extends GenericPoint>(
|
||||||
ellipse: Ellipse<Point>,
|
ellipse: Ellipse<Point>,
|
||||||
threshold = PRECISION,
|
threshold = PRECISION,
|
||||||
) => {
|
) => {
|
||||||
return distanceToEllipse(point, ellipse) <= threshold;
|
return ellipseDistance(point, ellipse) <= threshold;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ellipseFocusToCenter = <Point extends GenericPoint>(
|
export const ellipseDistance = <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 Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ellipseExtremes = <Point extends GenericPoint>(
|
|
||||||
ellipse: Ellipse<Point>,
|
|
||||||
) => {
|
|
||||||
const { center, angle } = ellipse;
|
|
||||||
const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
|
|
||||||
|
|
||||||
const majorAxis = widthGreaterThanHeight
|
|
||||||
? ellipse.halfWidth * 2
|
|
||||||
: ellipse.halfHeight * 2;
|
|
||||||
const minorAxis = widthGreaterThanHeight
|
|
||||||
? ellipse.halfHeight * 2
|
|
||||||
: ellipse.halfWidth * 2;
|
|
||||||
|
|
||||||
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,
|
p: Point,
|
||||||
ellipse: Ellipse<Point>,
|
ellipse: Ellipse<Point>,
|
||||||
) => {
|
) => {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
import { invariant } from "../excalidraw/utils";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
pointCenter,
|
pointCenter,
|
||||||
pointFromVector,
|
pointFromVector,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
|
pointsEqual,
|
||||||
} from "./point";
|
} from "./point";
|
||||||
import type { GenericPoint, Segment, Radians } from "./types";
|
import type { GenericPoint, Segment, Radians } from "./types";
|
||||||
import { PRECISION } from "./utils";
|
import { PRECISION } from "./utils";
|
||||||
|
@ -21,6 +23,11 @@ import {
|
||||||
* @returns The line segment delineated by the points
|
* @returns The line segment delineated by the points
|
||||||
*/
|
*/
|
||||||
export function segment<P extends GenericPoint>(a: P, b: P): Segment<P> {
|
export function segment<P extends GenericPoint>(a: P, b: P): Segment<P> {
|
||||||
|
invariant(
|
||||||
|
!pointsEqual(a, b),
|
||||||
|
"The start and end points of the segment cannot match",
|
||||||
|
);
|
||||||
|
|
||||||
return [a, b] as Segment<P>;
|
return [a, b] as Segment<P>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue