More refactor

This commit is contained in:
Mark Tolmacs 2024-09-25 11:59:27 +02:00
parent 392dd5b0b8
commit b697f63cad
No known key found for this signature in database
18 changed files with 206 additions and 199 deletions

View file

@ -13,15 +13,15 @@ import {
} from "../../packages/excalidraw/components/icons"; } from "../../packages/excalidraw/components/icons";
import { STORAGE_KEYS } from "../app_constants"; import { STORAGE_KEYS } from "../app_constants";
import { import {
isLineSegment, isSegment,
type GlobalPoint, type GlobalPoint,
type LineSegment, type Segment,
} from "../../packages/math"; } from "../../packages/math";
const renderLine = ( const renderLine = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
zoom: number, zoom: number,
segment: LineSegment<GlobalPoint>, segment: Segment<GlobalPoint>,
color: string, color: string,
) => { ) => {
context.save(); context.save();
@ -52,11 +52,11 @@ const render = (
) => { ) => {
frame.forEach((el: DebugElement) => { frame.forEach((el: DebugElement) => {
switch (true) { switch (true) {
case isLineSegment(el.data): case isSegment(el.data):
renderLine( renderLine(
context, context,
appState.zoom.value, appState.zoom.value,
el.data as LineSegment<GlobalPoint>, el.data as Segment<GlobalPoint>,
el.color, el.color,
); );
break; break;

View file

@ -65,7 +65,7 @@ import {
} from "./heading"; } from "./heading";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { import {
lineSegment, segment,
point, point,
pointRotateRads, pointRotateRads,
type GlobalPoint, type GlobalPoint,
@ -1615,7 +1615,7 @@ const intersectElementWithLine = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
): GlobalPoint[] | undefined => { ): GlobalPoint[] | undefined => {
if (isRectangularElement(element)) { if (isRectangularElement(element)) {
return segmentIntersectRectangleElement(element, lineSegment(a, b), gap); return segmentIntersectRectangleElement(element, segment(a, b), gap);
} }
const relateToCenter = relativizationToElementCenter(element, elementsMap); const relateToCenter = relativizationToElementCenter(element, elementsMap);

View file

@ -21,10 +21,10 @@ import type { Bounds } from "./bounds";
import { getElementAbsoluteCoords } from "./bounds"; import { getElementAbsoluteCoords } from "./bounds";
import { SIDE_RESIZING_THRESHOLD } from "../constants"; import { SIDE_RESIZING_THRESHOLD } from "../constants";
import { isLinearElement } from "./typeChecks"; import { isLinearElement } from "./typeChecks";
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math"; import type { GlobalPoint, Segment, LocalPoint } from "../../math";
import { import {
point, point,
pointOnLineSegment, segmentIncludesPoint,
pointRotateRads, pointRotateRads,
type Radians, type Radians,
} from "../../math"; } from "../../math";
@ -101,9 +101,9 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
for (const [dir, side] of Object.entries(sides)) { for (const [dir, side] of Object.entries(sides)) {
// test to see if x, y are on the line segment // test to see if x, y are on the line segment
if ( if (
pointOnLineSegment( segmentIncludesPoint(
point<Point>(x, y), point<Point>(x, y),
side as LineSegment<Point>, side as Segment<Point>,
SPACING, SPACING,
) )
) { ) {
@ -187,9 +187,9 @@ export const getTransformHandleTypeFromCoords = (
for (const [dir, side] of Object.entries(sides)) { for (const [dir, side] of Object.entries(sides)) {
// test to see if x, y are on the line segment // test to see if x, y are on the line segment
if ( if (
pointOnLineSegment( segmentIncludesPoint(
scenePointer, scenePointer,
side as LineSegment<GlobalPoint>, side as Segment<GlobalPoint>,
SPACING, SPACING,
) )
) { ) {

View file

@ -33,10 +33,10 @@ import {
isLinearElement, isLinearElement,
} from "./element/typeChecks"; } from "./element/typeChecks";
import type { ReadonlySetLike } from "./utility-types"; import type { ReadonlySetLike } from "./utility-types";
import type { GlobalPoint, LineSegment } from "../math"; import type { GlobalPoint, Segment } from "../math";
import { import {
isPointWithinBounds, isPointWithinBounds,
lineSegment, segment,
point, point,
pointRotateRads, pointRotateRads,
segmentsIntersectAt, segmentsIntersectAt,
@ -82,7 +82,7 @@ export const bindElementsToFramesAfterDuplication = (
const getElementLineSegments = ( const getElementLineSegments = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
): LineSegment<GlobalPoint>[] => { ): Segment<GlobalPoint>[] => {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element, element,
elementsMap, elementsMap,
@ -91,13 +91,13 @@ const getElementLineSegments = (
const center: GlobalPoint = point(cx, cy); const center: GlobalPoint = point(cx, cy);
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
const segments: LineSegment<GlobalPoint>[] = []; const segments: Segment<GlobalPoint>[] = [];
let i = 0; let i = 0;
while (i < element.points.length - 1) { while (i < element.points.length - 1) {
segments.push( segments.push(
lineSegment( segment(
pointRotateRads( pointRotateRads(
point( point(
element.points[i][0] + element.x, element.points[i][0] + element.x,
@ -137,35 +137,35 @@ const getElementLineSegments = (
if (element.type === "diamond") { if (element.type === "diamond") {
return [ return [
lineSegment(n, w), segment(n, w),
lineSegment(n, e), segment(n, e),
lineSegment(s, w), segment(s, w),
lineSegment(s, e), segment(s, e),
]; ];
} }
if (element.type === "ellipse") { if (element.type === "ellipse") {
return [ return [
lineSegment(n, w), segment(n, w),
lineSegment(n, e), segment(n, e),
lineSegment(s, w), segment(s, w),
lineSegment(s, e), segment(s, e),
lineSegment(n, w), segment(n, w),
lineSegment(n, e), segment(n, e),
lineSegment(s, w), segment(s, w),
lineSegment(s, e), segment(s, e),
]; ];
} }
return [ return [
lineSegment(nw, ne), segment(nw, ne),
lineSegment(sw, se), segment(sw, se),
lineSegment(nw, sw), segment(nw, sw),
lineSegment(ne, se), segment(ne, se),
lineSegment(nw, e), segment(nw, e),
lineSegment(sw, e), segment(sw, e),
lineSegment(ne, w), segment(ne, w),
lineSegment(se, w), segment(se, w),
]; ];
}; };

View file

@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { import {
fireEvent, fireEvent,
@ -27,8 +26,8 @@ import { KEYS } from "../keys";
import { getBoundTextElementPosition } from "../element/textElement"; import { getBoundTextElementPosition } from "../element/textElement";
import { createPasteEvent } from "../clipboard"; import { createPasteEvent } from "../clipboard";
import { arrayToMap, cloneJSON } from "../utils"; import { arrayToMap, cloneJSON } from "../utils";
import type { LocalPoint } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { point, type Radians } from "../../math"; import { point, radians } from "../../math";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@ -132,7 +131,7 @@ const createLinearElementWithCurveInsideMinMaxPoints = (
y: -2412.5069664197654, y: -2412.5069664197654,
width: 1750.4888916015625, width: 1750.4888916015625,
height: 410.51605224609375, height: 410.51605224609375,
angle: 0 as Radians, angle: radians(0),
strokeColor: "#000000", strokeColor: "#000000",
backgroundColor: "#fa5252", backgroundColor: "#fa5252",
fillStyle: "hachure", fillStyle: "hachure",

View file

@ -1,5 +1,5 @@
import type { LineSegment } from "../math"; import type { Segment } from "../math";
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math"; import { isSegment, segment, point, type GlobalPoint } from "../math";
import type { Bounds } from "./element/bounds"; import type { Bounds } from "./element/bounds";
import { isBounds } from "./element/typeChecks"; import { isBounds } from "./element/typeChecks";
@ -15,20 +15,20 @@ declare global {
export type DebugElement = { export type DebugElement = {
color: string; color: string;
data: LineSegment<GlobalPoint>; data: Segment<GlobalPoint>;
permanent: boolean; permanent: boolean;
}; };
export const debugDrawLine = ( export const debugDrawLine = (
segment: LineSegment<GlobalPoint> | LineSegment<GlobalPoint>[], segment: Segment<GlobalPoint> | Segment<GlobalPoint>[],
opts?: { opts?: {
color?: string; color?: string;
permanent?: boolean; permanent?: boolean;
}, },
) => { ) => {
const segments = ( const segments = (
isLineSegment(segment) ? [segment] : segment isSegment(segment) ? [segment] : segment
) as LineSegment<GlobalPoint>[]; ) as Segment<GlobalPoint>[];
segments.forEach((data) => segments.forEach((data) =>
addToCurrentFrame({ addToCurrentFrame({
@ -51,7 +51,7 @@ export const debugDrawPoint = (
const yOffset = opts?.fuzzy ? Math.random() * 3 : 0; const yOffset = opts?.fuzzy ? Math.random() * 3 : 0;
debugDrawLine( debugDrawLine(
lineSegment( segment(
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10), point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10),
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10), point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
), ),
@ -61,7 +61,7 @@ export const debugDrawPoint = (
}, },
); );
debugDrawLine( debugDrawLine(
lineSegment( segment(
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10), point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10),
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10), point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
), ),
@ -82,19 +82,19 @@ export const debugDrawBounds = (
(isBounds(box) ? [box] : box).forEach((bbox) => (isBounds(box) ? [box] : box).forEach((bbox) =>
debugDrawLine( debugDrawLine(
[ [
lineSegment( segment(
point<GlobalPoint>(bbox[0], bbox[1]), point<GlobalPoint>(bbox[0], bbox[1]),
point<GlobalPoint>(bbox[2], bbox[1]), point<GlobalPoint>(bbox[2], bbox[1]),
), ),
lineSegment( segment(
point<GlobalPoint>(bbox[2], bbox[1]), point<GlobalPoint>(bbox[2], bbox[1]),
point<GlobalPoint>(bbox[2], bbox[3]), point<GlobalPoint>(bbox[2], bbox[3]),
), ),
lineSegment( segment(
point<GlobalPoint>(bbox[2], bbox[3]), point<GlobalPoint>(bbox[2], bbox[3]),
point<GlobalPoint>(bbox[0], bbox[3]), point<GlobalPoint>(bbox[0], bbox[3]),
), ),
lineSegment( segment(
point<GlobalPoint>(bbox[0], bbox[3]), point<GlobalPoint>(bbox[0], bbox[3]),
point<GlobalPoint>(bbox[0], bbox[1]), point<GlobalPoint>(bbox[0], bbox[1]),
), ),

View file

@ -1,11 +1,11 @@
import { radians } from "./angle"; import { radians } from "./angle";
import { arc, isPointOnSymmetricArc } from "./arc"; import { arc, arcIncludesPoint } from "./arc";
import { point } from "./point"; import { point } from "./point";
describe("point on arc", () => { describe("point on arc", () => {
it("should detect point on simple arc", () => { it("should detect point on simple arc", () => {
expect( expect(
isPointOnSymmetricArc( arcIncludesPoint(
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
point(0.92291667, 0.385), point(0.92291667, 0.385),
), ),
@ -13,7 +13,7 @@ describe("point on arc", () => {
}); });
it("should not detect point outside of a simple arc", () => { it("should not detect point outside of a simple arc", () => {
expect( expect(
isPointOnSymmetricArc( arcIncludesPoint(
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
point(-0.92291667, 0.385), point(-0.92291667, 0.385),
), ),
@ -21,7 +21,7 @@ describe("point on arc", () => {
}); });
it("should not detect point with good angle but incorrect radius", () => { it("should not detect point with good angle but incorrect radius", () => {
expect( expect(
isPointOnSymmetricArc( arcIncludesPoint(
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)), arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
point(-0.5, 0.5), point(-0.5, 0.5),
), ),

View file

@ -1,7 +1,7 @@
import { cartesian2Polar, radians } from "./angle"; import { cartesian2Polar, radians } from "./angle";
import { ellipse, interceptPointsOfLineAndEllipse } from "./ellipse"; import { ellipse, ellipseSegmentInterceptPoints } from "./ellipse";
import { point } from "./point"; import { point } from "./point";
import type { GenericPoint, LineSegment, Radians, SymmetricArc } from "./types"; import type { GenericPoint, Segment, Radians, Arc } from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
/** /**
@ -20,15 +20,15 @@ export function arc<Point extends GenericPoint>(
startAngle: Radians, startAngle: Radians,
endAngle: Radians, endAngle: Radians,
) { ) {
return { center, radius, startAngle, endAngle } as SymmetricArc<Point>; return { center, radius, startAngle, endAngle } as Arc<Point>;
} }
/** /**
* Determines if a cartesian point lies on a symmetric arc, i.e. an arc which * Determines if a cartesian point lies on a symmetric arc, i.e. an arc which
* is part of a circle contour centered on 0, 0. * is part of a circle contour centered on 0, 0.
*/ */
export function isPointOnSymmetricArc<P extends GenericPoint>( export function arcIncludesPoint<P extends GenericPoint>(
{ center, radius: arcRadius, startAngle, endAngle }: SymmetricArc<P>, { center, radius: arcRadius, startAngle, endAngle }: Arc<P>,
p: P, p: P,
): boolean { ): boolean {
const [radius, angle] = cartesian2Polar( const [radius, angle] = cartesian2Polar(
@ -47,10 +47,10 @@ export function isPointOnSymmetricArc<P extends GenericPoint>(
* point and end point and a symmetric arc. * point and end point and a symmetric arc.
*/ */
export function interceptOfSymmetricArcAndSegment<Point extends GenericPoint>( export function interceptOfSymmetricArcAndSegment<Point extends GenericPoint>(
a: Readonly<SymmetricArc<Point>>, a: Readonly<Arc<Point>>,
l: Readonly<LineSegment<Point>>, l: Readonly<Segment<Point>>,
): Point[] { ): Point[] {
return interceptPointsOfLineAndEllipse( return ellipseSegmentInterceptPoints(
ellipse(a.center, radians(0), a.radius, a.radius), ellipse(a.center, radians(0), a.radius, a.radius),
l, l,
).filter((candidate) => { ).filter((candidate) => {

View file

@ -1,12 +1,12 @@
import { radians } from "./angle"; import { radians } from "./angle";
import { import {
ellipse, ellipse,
interceptPointsOfLineAndEllipse, ellipseSegmentInterceptPoints,
pointInEllipse, ellipseIncludesPoint,
pointOnEllipse, ellipseTouchesPoint,
} from "./ellipse"; } from "./ellipse";
import { point } from "./point"; import { point } from "./point";
import { lineSegment } from "./segment"; import { segment } from "./segment";
import type { Ellipse, GlobalPoint } from "./types"; import type { Ellipse, GlobalPoint } from "./types";
describe("point and ellipse", () => { describe("point and ellipse", () => {
@ -14,41 +14,44 @@ describe("point and ellipse", () => {
it("point on ellipse", () => { it("point on ellipse", () => {
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => { [point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
expect(pointOnEllipse(p, target)).toBe(true); expect(ellipseTouchesPoint(p, target)).toBe(true);
}); });
expect(pointOnEllipse(point(-1.4, 0.7), target, 0.1)).toBe(true); expect(ellipseTouchesPoint(point(-1.4, 0.7), target, 0.1)).toBe(true);
expect(pointOnEllipse(point(-1.4, 0.71), target, 0.01)).toBe(true); expect(ellipseTouchesPoint(point(-1.4, 0.71), target, 0.01)).toBe(true);
expect(pointOnEllipse(point(1.4, 0.7), target, 0.1)).toBe(true); expect(ellipseTouchesPoint(point(1.4, 0.7), target, 0.1)).toBe(true);
expect(pointOnEllipse(point(1.4, 0.71), target, 0.01)).toBe(true); expect(ellipseTouchesPoint(point(1.4, 0.71), target, 0.01)).toBe(true);
expect(pointOnEllipse(point(1, -0.86), target, 0.1)).toBe(true); expect(ellipseTouchesPoint(point(1, -0.86), target, 0.1)).toBe(true);
expect(pointOnEllipse(point(1, -0.86), target, 0.01)).toBe(true); expect(ellipseTouchesPoint(point(1, -0.86), target, 0.01)).toBe(true);
expect(pointOnEllipse(point(-1, -0.86), target, 0.1)).toBe(true); expect(ellipseTouchesPoint(point(-1, -0.86), target, 0.1)).toBe(true);
expect(pointOnEllipse(point(-1, -0.86), target, 0.01)).toBe(true); expect(ellipseTouchesPoint(point(-1, -0.86), target, 0.01)).toBe(true);
expect(pointOnEllipse(point(-1, 0.8), target)).toBe(false); expect(ellipseTouchesPoint(point(-1, 0.8), target)).toBe(false);
expect(pointOnEllipse(point(1, -0.8), target)).toBe(false); expect(ellipseTouchesPoint(point(1, -0.8), target)).toBe(false);
}); });
it("point in ellipse", () => { it("point in ellipse", () => {
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => { [point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => {
expect(pointInEllipse(p, target)).toBe(true); expect(ellipseIncludesPoint(p, target)).toBe(true);
}); });
expect(pointInEllipse(point(-1, 0.8), target)).toBe(true); expect(ellipseIncludesPoint(point(-1, 0.8), target)).toBe(true);
expect(pointInEllipse(point(1, -0.8), target)).toBe(true); expect(ellipseIncludesPoint(point(1, -0.8), target)).toBe(true);
expect(pointInEllipse(point(-1, 1), target)).toBe(false); // Point on outline
expect(pointInEllipse(point(-1.4, 0.8), target)).toBe(false); expect(ellipseIncludesPoint(point(2, 0), target)).toBe(true);
expect(ellipseIncludesPoint(point(-1, 1), target)).toBe(false);
expect(ellipseIncludesPoint(point(-1.4, 0.8), target)).toBe(false);
}); });
}); });
describe("line and ellipse", () => { describe("line and ellipse", () => {
it("detects outside segment", () => { it("detects outside segment", () => {
const l = lineSegment<GlobalPoint>(point(-100, 0), point(-10, 0)); 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(interceptPointsOfLineAndEllipse(e, l).length).toBe(0); expect(ellipseSegmentInterceptPoints(e, l).length).toBe(0);
}); });
}); });

View file

@ -6,7 +6,7 @@ import {
pointFromVector, pointFromVector,
pointRotateRads, pointRotateRads,
} from "./point"; } from "./point";
import type { Ellipse, GenericPoint, LineSegment, Radians } from "./types"; import type { Ellipse, GenericPoint, Segment, Radians } from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
import { import {
vector, vector,
@ -16,6 +16,15 @@ import {
vectorScale, vectorScale,
} from "./vector"; } from "./vector";
/**
* Construct an Ellipse object from the parameters
*
* @param center The center of the ellipse
* @param angle The slanting of the ellipse in radians
* @param halfWidth Half of the width of a non-slanted version of the ellipse
* @param halfHeight Half of the height of a non-slanted version of the ellipse
* @returns The constructed Ellipse object
*/
export function ellipse<Point extends GenericPoint>( export function ellipse<Point extends GenericPoint>(
center: Point, center: Point,
angle: Radians, angle: Radians,
@ -30,7 +39,14 @@ export function ellipse<Point extends GenericPoint>(
} as Ellipse<Point>; } as Ellipse<Point>;
} }
export const pointInEllipse = <Point extends GenericPoint>( /**
* Determines if a point is inside or on the ellipse outline
*
* @param p The point to test
* @param ellipse The ellipse to compare against
* @returns TRUE if the point is inside or on the outline of the ellipse
*/
export const ellipseIncludesPoint = <Point extends GenericPoint>(
p: Point, p: Point,
ellipse: Ellipse<Point>, ellipse: Ellipse<Point>,
) => { ) => {
@ -52,7 +68,16 @@ export const pointInEllipse = <Point extends GenericPoint>(
); );
}; };
export const pointOnEllipse = <Point extends GenericPoint>( /**
* Tests whether a point lies on the outline of the ellipse within a given
* tolerance
*
* @param point The point to test
* @param ellipse The ellipse to compare against
* @param threshold The distance to consider a point close enough to be "on" the outline
* @returns TRUE if the point is on the ellise outline
*/
export const ellipseTouchesPoint = <Point extends GenericPoint>(
point: Point, point: Point,
ellipse: Ellipse<Point>, ellipse: Ellipse<Point>,
threshold = PRECISION, threshold = PRECISION,
@ -60,7 +85,7 @@ export const pointOnEllipse = <Point extends GenericPoint>(
return distanceToEllipse(point, ellipse) <= threshold; return distanceToEllipse(point, ellipse) <= threshold;
}; };
export const ellipseAxes = <Point extends GenericPoint>( export const ellipseFocusToCenter = <Point extends GenericPoint>(
ellipse: Ellipse<Point>, ellipse: Ellipse<Point>,
) => { ) => {
const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight; const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
@ -72,17 +97,6 @@ export const ellipseAxes = <Point extends GenericPoint>(
? ellipse.halfHeight * 2 ? ellipse.halfHeight * 2
: ellipse.halfWidth * 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); return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
}; };
@ -90,7 +104,14 @@ export const ellipseExtremes = <Point extends GenericPoint>(
ellipse: Ellipse<Point>, ellipse: Ellipse<Point>,
) => { ) => {
const { center, angle } = ellipse; const { center, angle } = ellipse;
const { majorAxis, minorAxis } = ellipseAxes(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 cos = Math.cos(angle);
const sin = Math.sin(angle); const sin = Math.sin(angle);
@ -175,9 +196,9 @@ const distanceToEllipse = <Point extends GenericPoint>(
* Calculate a maximum of two intercept points for a line going throug an * Calculate a maximum of two intercept points for a line going throug an
* ellipse. * ellipse.
*/ */
export function interceptPointsOfLineAndEllipse<Point extends GenericPoint>( export function ellipseSegmentInterceptPoints<Point extends GenericPoint>(
e: Readonly<Ellipse<Point>>, e: Readonly<Ellipse<Point>>,
l: Readonly<LineSegment<Point>>, l: Readonly<Segment<Point>>,
): Point[] { ): Point[] {
const rx = e.halfWidth; const rx = e.halfWidth;
const ry = e.halfHeight; const ry = e.halfHeight;

View file

@ -1,5 +1,5 @@
import { pointsEqual } from "./point"; import { pointsEqual } from "./point";
import { lineSegment, pointOnLineSegment } from "./segment"; import { segment, segmentIncludesPoint } from "./segment";
import type { GenericPoint, Polygon } from "./types"; import type { GenericPoint, Polygon } from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
@ -44,7 +44,7 @@ export const pointOnPolygon = <Point extends GenericPoint>(
let on = false; let on = false;
for (let i = 0, l = poly.length - 1; i < l; i++) { for (let i = 0, l = poly.length - 1; i < l; i++) {
if (pointOnLineSegment(p, lineSegment(poly[i], poly[i + 1]), threshold)) { if (segmentIncludesPoint(p, segment(poly[i], poly[i + 1]), threshold)) {
on = true; on = true;
break; break;
} }

View file

@ -1,15 +1,15 @@
import { point } from "./point"; import { point } from "./point";
import { lineSegment, segmentsIntersectAt } from "./segment"; import { segment, segmentsIntersectAt } from "./segment";
import type { GlobalPoint, LineSegment } from "./types"; import type { GlobalPoint, Segment } from "./types";
describe("segment intersects segment", () => { describe("segment intersects segment", () => {
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4)); const lineA: Segment<GlobalPoint> = segment(point(1, 4), point(3, 4));
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7)); const lineB: Segment<GlobalPoint> = segment(point(2, 1), point(2, 7));
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8)); const lineC: Segment<GlobalPoint> = segment(point(1, 8), point(3, 8));
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8)); const lineD: Segment<GlobalPoint> = segment(point(1, 8), point(3, 8));
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9)); const lineE: Segment<GlobalPoint> = segment(point(1, 9), point(3, 9));
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4)); const lineF: Segment<GlobalPoint> = segment(point(1, 2), point(3, 4));
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3)); const lineG: Segment<GlobalPoint> = segment(point(0, 1), point(2, 3));
it("intersection", () => { it("intersection", () => {
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]); expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);

View file

@ -4,7 +4,7 @@ import {
pointFromVector, pointFromVector,
pointRotateRads, pointRotateRads,
} from "./point"; } from "./point";
import type { GenericPoint, LineSegment, Radians } from "./types"; import type { GenericPoint, Segment, Radians } from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
import { import {
vectorAdd, vectorAdd,
@ -20,18 +20,15 @@ import {
* @param points The two points delimiting the line segment on each end * @param points The two points delimiting the line segment on each end
* @returns The line segment delineated by the points * @returns The line segment delineated by the points
*/ */
export function lineSegment<P extends GenericPoint>( export function segment<P extends GenericPoint>(a: P, b: P): Segment<P> {
a: P, return [a, b] as Segment<P>;
b: P,
): LineSegment<P> {
return [a, b] as LineSegment<P>;
} }
export function lineSegmentFromPointArray<P extends GenericPoint>( export function segmentFromPointArray<P extends GenericPoint>(
pointArray: P[], pointArray: P[],
): LineSegment<P> | undefined { ): Segment<P> | undefined {
return pointArray.length === 2 return pointArray.length === 2
? lineSegment<P>(pointArray[0], pointArray[1]) ? segment<P>(pointArray[0], pointArray[1])
: undefined; : undefined;
} }
@ -40,9 +37,9 @@ export function lineSegmentFromPointArray<P extends GenericPoint>(
* @param segment * @param segment
* @returns * @returns
*/ */
export const isLineSegment = <Point extends GenericPoint>( export const isSegment = <Point extends GenericPoint>(
segment: unknown, segment: unknown,
): segment is LineSegment<Point> => ): segment is Segment<Point> =>
Array.isArray(segment) && Array.isArray(segment) &&
segment.length === 2 && segment.length === 2 &&
isPoint(segment[0]) && isPoint(segment[0]) &&
@ -57,12 +54,12 @@ export const isLineSegment = <Point extends GenericPoint>(
* @param origin * @param origin
* @returns * @returns
*/ */
export const lineSegmentRotate = <Point extends GenericPoint>( export const segmentRotate = <Point extends GenericPoint>(
l: LineSegment<Point>, l: Segment<Point>,
angle: Radians, angle: Radians,
origin?: Point, origin?: Point,
): LineSegment<Point> => { ): Segment<Point> => {
return lineSegment( return segment(
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle), pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle), pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
); );
@ -73,8 +70,8 @@ export const lineSegmentRotate = <Point extends GenericPoint>(
* intersect at. * intersect at.
*/ */
export const segmentsIntersectAt = <Point extends GenericPoint>( export const segmentsIntersectAt = <Point extends GenericPoint>(
a: Readonly<LineSegment<Point>>, a: Readonly<Segment<Point>>,
b: Readonly<LineSegment<Point>>, b: Readonly<Segment<Point>>,
): Point | null => { ): Point | null => {
const a0 = vectorFromPoint(a[0]); const a0 = vectorFromPoint(a[0]);
const a1 = vectorFromPoint(a[1]); const a1 = vectorFromPoint(a[1]);
@ -105,12 +102,12 @@ export const segmentsIntersectAt = <Point extends GenericPoint>(
return null; return null;
}; };
export const pointOnLineSegment = <Point extends GenericPoint>( export const segmentIncludesPoint = <Point extends GenericPoint>(
point: Point, point: Point,
line: LineSegment<Point>, line: Segment<Point>,
threshold = PRECISION, threshold = PRECISION,
) => { ) => {
const distance = distanceToLineSegment(point, line); const distance = segmentDistanceToPoint(point, line);
if (distance === 0) { if (distance === 0) {
return true; return true;
@ -119,9 +116,9 @@ export const pointOnLineSegment = <Point extends GenericPoint>(
return distance < threshold; return distance < threshold;
}; };
export const distanceToLineSegment = <Point extends GenericPoint>( export const segmentDistanceToPoint = <Point extends GenericPoint>(
point: Point, point: Point,
line: LineSegment<Point>, line: Segment<Point>,
) => { ) => {
const [x, y] = point; const [x, y] = point;
const [[x1, y1], [x2, y2]] = line; const [[x1, y1], [x2, y2]] = line;

View file

@ -67,8 +67,8 @@ export type Line<P extends GenericPoint> = [p: P, q: P] & {
* line that is bounded by two distinct end points, and * line that is bounded by two distinct end points, and
* contains every point on the line that is between its endpoints. * contains every point on the line that is between its endpoints.
*/ */
export type LineSegment<P extends GenericPoint> = [a: P, b: P] & { export type Segment<P extends GenericPoint> = [a: P, b: P] & {
_brand: "excalimath_linesegment"; _brand: "excalimath_segment";
}; };
/** /**
@ -81,23 +81,14 @@ export type Vector = [u: number, v: number] & {
/** /**
* A triangle represented by 3 points * A triangle represented by 3 points
*/ */
export type Triangle<P extends GlobalPoint | LocalPoint | ViewportPoint> = [ export type Triangle<P extends GenericPoint> = [a: P, b: P, c: P] & {
a: P,
b: P,
c: P,
] & {
_brand: "excalimath__triangle"; _brand: "excalimath__triangle";
}; };
/** /**
* A rectangular shape represented by 4 points at its corners * A rectangular shape represented by 4 points at its corners
*/ */
export type Rectangle<P extends GlobalPoint | LocalPoint | ViewportPoint> = [ export type Rectangle<P extends GenericPoint> = [a: P, b: P, c: P, d: P] & {
a: P,
b: P,
c: P,
d: P,
] & {
_brand: "excalimath__rectangle"; _brand: "excalimath__rectangle";
}; };
@ -105,28 +96,24 @@ export type Rectangle<P extends GlobalPoint | LocalPoint | ViewportPoint> = [
* A polygon is a closed shape by connecting the given points * A polygon is a closed shape by connecting the given points
* rectangles and diamonds are modelled by polygons * rectangles and diamonds are modelled by polygons
*/ */
export type Polygon<Point extends GlobalPoint | LocalPoint | ViewportPoint> = export type Polygon<Point extends GenericPoint> = Point[] & {
Point[] & {
_brand: "excalimath_polygon"; _brand: "excalimath_polygon";
}; };
/** /**
* Cubic bezier curve with four control points * Cubic bezier curve with four control points
*/ */
export type Curve<Point extends GlobalPoint | LocalPoint | ViewportPoint> = [ export type Curve<Point extends GenericPoint> = [Point, Point, Point, Point] & {
Point,
Point,
Point,
Point,
] & {
_brand: "excalimath_curve"; _brand: "excalimath_curve";
}; };
/** /**
* Represents a symmetric arc, a segment of a circular path
*
* Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle * Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle
* corresponds to (1, 0) cartesian coordinates (point), i.e. to the "right" * corresponds to (1, 0) cartesian coordinates (point), i.e. to the "right"
*/ */
export type SymmetricArc<Point extends GenericPoint> = { export type Arc<Point extends GenericPoint> = {
center: Point; center: Point;
radius: number; radius: number;
startAngle: Radians; startAngle: Radians;

View file

@ -2,8 +2,8 @@ import type { Curve, Degrees, GlobalPoint } from "../math";
import { import {
curve, curve,
degreesToRadians, degreesToRadians,
lineSegment, segment,
lineSegmentRotate, segmentRotate,
point, point,
pointRotateDegs, pointRotateDegs,
} from "../math"; } from "../math";
@ -34,10 +34,10 @@ describe("point and curve", () => {
describe("point and polylines", () => { describe("point and polylines", () => {
const polyline: Polyline<GlobalPoint> = [ const polyline: Polyline<GlobalPoint> = [
lineSegment(point(1, 0), point(1, 2)), segment(point(1, 0), point(1, 2)),
lineSegment(point(1, 2), point(2, 2)), segment(point(1, 2), point(2, 2)),
lineSegment(point(2, 2), point(2, 1)), segment(point(2, 2), point(2, 1)),
lineSegment(point(2, 1), point(3, 1)), segment(point(2, 1), point(3, 1)),
]; ];
it("point on the line", () => { it("point on the line", () => {
@ -68,7 +68,7 @@ describe("point and polylines", () => {
const rotation = (Math.random() * 360) as Degrees; const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation); const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
const rotatedPolyline = polyline.map((line) => const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)), segmentRotate(line, degreesToRadians(rotation), point(0, 0)),
); );
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
}); });
@ -79,7 +79,7 @@ describe("point and polylines", () => {
const rotation = (Math.random() * 360) as Degrees; const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation); const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation);
const rotatedPolyline = polyline.map((line) => const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)), segmentRotate(line, degreesToRadians(rotation), point(0, 0)),
); );
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
}); });

View file

@ -2,14 +2,14 @@ import type { Polycurve, Polyline } from "./geometry/shape";
import { type GeometricShape } from "./geometry/shape"; import { type GeometricShape } from "./geometry/shape";
import type { Curve, GenericPoint } from "../math"; import type { Curve, GenericPoint } from "../math";
import { import {
lineSegment, segment,
point, point,
polygonIncludesPoint, polygonIncludesPoint,
pointOnLineSegment, segmentIncludesPoint,
pointOnPolygon, pointOnPolygon,
polygonFromPoints, polygonFromPoints,
pointOnEllipse, ellipseTouchesPoint,
pointInEllipse, ellipseIncludesPoint,
} 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
@ -22,9 +22,9 @@ export const isPointOnShape = <Point extends GenericPoint>(
case "polygon": case "polygon":
return pointOnPolygon(point, shape.data, tolerance); return pointOnPolygon(point, shape.data, tolerance);
case "ellipse": case "ellipse":
return pointOnEllipse(point, shape.data, tolerance); return ellipseTouchesPoint(point, shape.data, tolerance);
case "line": case "line":
return pointOnLineSegment(point, shape.data, tolerance); return segmentIncludesPoint(point, shape.data, tolerance);
case "polyline": case "polyline":
return pointOnPolyline(point, shape.data, tolerance); return pointOnPolyline(point, shape.data, tolerance);
case "curve": case "curve":
@ -49,7 +49,7 @@ export const isPointInShape = <Point extends GenericPoint>(
case "curve": case "curve":
return false; return false;
case "ellipse": case "ellipse":
return pointInEllipse(p, shape.data); return ellipseIncludesPoint(p, shape.data);
case "polyline": { case "polyline": {
const polygon = polygonFromPoints(shape.data.flat()); const polygon = polygonFromPoints(shape.data.flat());
return polygonIncludesPoint(p, polygon); return polygonIncludesPoint(p, polygon);
@ -96,7 +96,7 @@ const polyLineFromCurve = <Point extends GenericPoint>(
t += increment; t += increment;
if (t <= 1) { if (t <= 1) {
const nextPoint: Point = point(equation(t, 0), equation(t, 1)); const nextPoint: Point = point(equation(t, 0), equation(t, 1));
lineSegments.push(lineSegment(startingPoint, nextPoint)); lineSegments.push(segment(startingPoint, nextPoint));
startingPoint = nextPoint; startingPoint = nextPoint;
} }
} }
@ -117,5 +117,5 @@ export const pointOnPolyline = <Point extends GenericPoint>(
polyline: Polyline<Point>, polyline: Polyline<Point>,
threshold = 10e-5, threshold = 10e-5,
) => { ) => {
return polyline.some((line) => pointOnLineSegment(point, line, threshold)); return polyline.some((line) => segmentIncludesPoint(point, line, threshold));
}; };

View file

@ -1,20 +1,20 @@
import type { GlobalPoint, LineSegment, Polygon } from "../../math"; import type { GlobalPoint, Segment, Polygon } from "../../math";
import { import {
point, point,
lineSegment, segment,
polygon, polygon,
pointOnLineSegment, segmentIncludesPoint,
pointOnPolygon, pointOnPolygon,
polygonIncludesPoint, polygonIncludesPoint,
} from "../../math"; } from "../../math";
describe("point and line", () => { describe("point and line", () => {
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2)); const s: Segment<GlobalPoint> = segment(point(1, 0), point(1, 2));
it("point on the line", () => { it("point on the line", () => {
expect(pointOnLineSegment(point(0, 1), s)).toBe(false); expect(segmentIncludesPoint(point(0, 1), s)).toBe(false);
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true); expect(segmentIncludesPoint(point(1, 1), s, 0)).toBe(true);
expect(pointOnLineSegment(point(2, 1), s)).toBe(false); expect(segmentIncludesPoint(point(2, 1), s)).toBe(false);
}); });
}); });

View file

@ -16,7 +16,7 @@ import type {
Curve, Curve,
Ellipse, Ellipse,
GenericPoint, GenericPoint,
LineSegment, Segment,
Polygon, Polygon,
Radians, Radians,
ViewportPoint, ViewportPoint,
@ -24,7 +24,7 @@ import type {
import { import {
curve, curve,
ellipse, ellipse,
lineSegment, segment,
point, point,
pointFromArray, pointFromArray,
pointFromVector, pointFromVector,
@ -63,7 +63,7 @@ import { invariant } from "../../excalidraw/utils";
// this corresponds to a straight line element in the editor but it could also // this corresponds to a straight line element in the editor but it could also
// be used to model other elements // be used to model other elements
export type Polyline<Point extends GlobalPoint | LocalPoint | ViewportPoint> = export type Polyline<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
LineSegment<Point>[]; Segment<Point>[];
// a polycurve is a curve consisting of ther curves, this corresponds to a complex // a polycurve is a curve consisting of ther curves, this corresponds to a complex
// curve on the canvas // curve on the canvas
@ -73,7 +73,7 @@ export type Polycurve<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
export type GeometricShape<Point extends GenericPoint> = export type GeometricShape<Point extends GenericPoint> =
| { | {
type: "line"; type: "line";
data: LineSegment<Point>; data: Segment<Point>;
} }
| { | {
type: "polygon"; type: "polygon";
@ -242,11 +242,11 @@ const polylineFromPoints = <
points: Point[], points: Point[],
): Polyline<Point> => { ): Polyline<Point> => {
let previousPoint: Point = points[0]; let previousPoint: Point = points[0];
const polyline: LineSegment<Point>[] = []; const polyline: Segment<Point>[] = [];
for (let i = 1; i < points.length; i++) { for (let i = 1; i < points.length; i++) {
const nextPoint = points[i]; const nextPoint = points[i];
polyline.push(lineSegment<Point>(previousPoint, nextPoint)); polyline.push(segment<Point>(previousPoint, nextPoint));
previousPoint = nextPoint; previousPoint = nextPoint;
} }
@ -356,7 +356,7 @@ export const segmentIntersectRectangleElement = <
Point extends LocalPoint | GlobalPoint, Point extends LocalPoint | GlobalPoint,
>( >(
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
segment: LineSegment<Point>, s: Segment<Point>,
gap: number = 0, gap: number = 0,
): Point[] => { ): Point[] => {
const bounds = [ const bounds = [
@ -371,23 +371,23 @@ export const segmentIntersectRectangleElement = <
); );
return [ return [
lineSegment( segment(
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle), pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle), pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
), ),
lineSegment( segment(
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle), pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle), pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
), ),
lineSegment( segment(
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle), pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle), pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
), ),
lineSegment( segment(
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle), pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle), pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
), ),
] ]
.map((s) => segmentsIntersectAt(segment, s)) .map((l) => segmentsIntersectAt(s, l))
.filter((i): i is Point => !!i); .filter((i): i is Point => !!i);
}; };