Arc tests

pull/8539/head
Mark Tolmacs 5 months ago
parent f79fb899fc
commit 3fe73e79a6
No known key found for this signature in database

@ -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") {

@ -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…
Cancel
Save