Further math refactor and simplifications

This commit is contained in:
Mark Tolmacs 2024-09-23 17:13:40 +02:00
parent 41885b4bb3
commit 0e2f8c958e
No known key found for this signature in database
18 changed files with 262 additions and 175 deletions

View file

@ -48,7 +48,6 @@ import type { MarkOptional, Mutable } from "../utility-types";
import { detectLineHeight, getContainerElement } from "../element/textElement"; import { detectLineHeight, getContainerElement } from "../element/textElement";
import { normalizeLink } from "./url"; import { normalizeLink } from "./url";
import { syncInvalidIndices } from "../fractionalIndex"; import { syncInvalidIndices } from "../fractionalIndex";
import { getSizeFromPoints } from "../points";
import { getLineHeight } from "../fonts"; import { getLineHeight } from "../fonts";
import { normalizeFixedPoint } from "../element/binding"; import { normalizeFixedPoint } from "../element/binding";
import { import {
@ -57,7 +56,7 @@ import {
getNormalizedZoom, getNormalizedZoom,
} from "../scene"; } from "../scene";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { isFiniteNumber, point } from "../../math"; import { pointExtent, isFiniteNumber, point } from "../../math";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@ -288,7 +287,7 @@ const restoreElement = (
points, points,
x, x,
y, y,
...getSizeFromPoints(points), ...pointExtent(points),
}); });
case "arrow": { case "arrow": {
const { startArrowhead = null, endArrowhead = "arrow" } = element; const { startArrowhead = null, endArrowhead = "arrow" } = element;
@ -315,7 +314,7 @@ const restoreElement = (
x, x,
y, y,
elbowed: (element as ExcalidrawArrowElement).elbowed, elbowed: (element as ExcalidrawArrowElement).elbowed,
...getSizeFromPoints(points), ...pointExtent(points),
}); });
} }

View file

@ -48,12 +48,11 @@ import {
getFontString, getFontString,
toBrandedType, toBrandedType,
} from "../utils"; } from "../utils";
import { getSizeFromPoints } from "../points";
import { randomId } from "../random"; import { randomId } from "../random";
import { syncInvalidIndices } from "../fractionalIndex"; import { syncInvalidIndices } from "../fractionalIndex";
import { getLineHeight } from "../fonts"; import { getLineHeight } from "../fonts";
import { isArrowElement } from "../element/typeChecks"; import { isArrowElement } from "../element/typeChecks";
import { point, type LocalPoint } from "../../math"; import { pointExtent, point, type LocalPoint } from "../../math";
export type ValidLinearElement = { export type ValidLinearElement = {
type: "arrow" | "line"; type: "arrow" | "line";
@ -556,7 +555,7 @@ export const convertToExcalidrawElements = (
Object.assign( Object.assign(
excalidrawElement, excalidrawElement,
getSizeFromPoints(excalidrawElement.points), pointExtent(excalidrawElement.points),
); );
break; break;
} }

View file

@ -19,7 +19,6 @@ import {
isLinearElement, isLinearElement,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { rescalePoints } from "../points";
import { getBoundTextElement, getContainerElement } from "./textElement"; import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
@ -38,6 +37,7 @@ import {
pointDistance, pointDistance,
pointFromArray, pointFromArray,
pointRotateRads, pointRotateRads,
pointRescaleFromTopLeft,
} from "../../math"; } from "../../math";
import type { Mutable } from "../utility-types"; import type { Mutable } from "../utility-types";
@ -859,10 +859,10 @@ export const getResizedElementAbsoluteCoords = (
]; ];
} }
const points = rescalePoints( const points = pointRescaleFromTopLeft(
0, 0,
nextWidth, nextWidth,
rescalePoints(1, nextHeight, element.points, normalizePoints), pointRescaleFromTopLeft(1, nextHeight, element.points, normalizePoints),
normalizePoints, normalizePoints,
); );

View file

@ -1,10 +1,10 @@
import type { ExcalidrawElement } from "./types"; import type { ExcalidrawElement } from "./types";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { getSizeFromPoints } from "../points";
import { randomInteger } from "../random"; import { randomInteger } from "../random";
import { getUpdatedTimestamp } from "../utils"; import { getUpdatedTimestamp } from "../utils";
import type { Mutable } from "../utility-types"; import type { Mutable } from "../utility-types";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
import { pointExtent } from "../../math";
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit< export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>, Partial<TElement>,
@ -27,7 +27,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
const { points, fileId } = updates as any; const { points, fileId } = updates as any;
if (typeof points !== "undefined") { if (typeof points !== "undefined") {
updates = { ...getSizeFromPoints(points), ...updates }; updates = { ...pointExtent(points), ...updates };
} }
for (const key in updates) { for (const key in updates) {

View file

@ -1,5 +1,4 @@
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants"; import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
import { rescalePoints } from "../points";
import type { import type {
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
@ -62,6 +61,7 @@ import {
pointFromPair, pointFromPair,
pointRotateRads, pointRotateRads,
type Radians, type Radians,
pointRescaleFromTopLeft,
} from "../../math"; } from "../../math";
// Returns true when transform (resizing/rotation) happened // Returns true when transform (resizing/rotation) happened
@ -191,10 +191,10 @@ export const rescalePointsInElement = (
) => ) =>
isLinearElement(element) || isFreeDrawElement(element) isLinearElement(element) || isFreeDrawElement(element)
? { ? {
points: rescalePoints( points: pointRescaleFromTopLeft(
0, 0,
width, width,
rescalePoints(1, height, element.points, normalizePoints), pointRescaleFromTopLeft(1, height, element.points, normalizePoints),
normalizePoints, normalizePoints,
), ),
} }
@ -674,14 +674,14 @@ export const resizeSingleElement = (
let rescaledElementPointsY; let rescaledElementPointsY;
let rescaledPoints; let rescaledPoints;
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
rescaledElementPointsY = rescalePoints( rescaledElementPointsY = pointRescaleFromTopLeft(
1, 1,
eleNewHeight, eleNewHeight,
(stateAtResizeStart as ExcalidrawLinearElement).points, (stateAtResizeStart as ExcalidrawLinearElement).points,
true, true,
); );
rescaledPoints = rescalePoints( rescaledPoints = pointRescaleFromTopLeft(
0, 0,
eleNewWidth, eleNewWidth,
rescaledElementPointsY, rescaledElementPointsY,

View file

@ -1,5 +1,6 @@
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { import {
pointExtent,
point, point,
pointScaleFromOrigin, pointScaleFromOrigin,
pointTranslate, pointTranslate,
@ -12,7 +13,6 @@ import {
type Vector, type Vector,
} from "../../math"; } from "../../math";
import BinaryHeap from "../binaryheap"; import BinaryHeap from "../binaryheap";
import { getSizeFromPoints } from "../points";
import { aabbForElement, pointInsideBounds } from "../shapes"; import { aabbForElement, pointInsideBounds } from "../shapes";
import { isAnyTrue, toBrandedType } from "../utils"; import { isAnyTrue, toBrandedType } from "../utils";
import { import {
@ -955,7 +955,7 @@ const normalizedArrowElementUpdate = (
points, points,
x: offsetX + (externalOffsetX ?? 0), x: offsetX + (externalOffsetX ?? 0),
y: offsetY + (externalOffsetY ?? 0), y: offsetY + (externalOffsetY ?? 0),
...getSizeFromPoints(points), ...pointExtent(points),
}; };
}; };

View file

@ -1,59 +0,0 @@
import { pointFromPair, type GlobalPoint, type LocalPoint } from "../math";
export const getSizeFromPoints = (
points: readonly (GlobalPoint | LocalPoint)[],
) => {
const xs = points.map((point) => point[0]);
const ys = points.map((point) => point[1]);
return {
width: Math.max(...xs) - Math.min(...xs),
height: Math.max(...ys) - Math.min(...ys),
};
};
/** @arg dimension, 0 for rescaling only x, 1 for y */
export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
dimension: 0 | 1,
newSize: number,
points: readonly Point[],
normalize: boolean,
): Point[] => {
const coordinates = points.map((point) => point[dimension]);
const maxCoordinate = Math.max(...coordinates);
const minCoordinate = Math.min(...coordinates);
const size = maxCoordinate - minCoordinate;
const scale = size === 0 ? 1 : newSize / size;
let nextMinCoordinate = Infinity;
const scaledPoints = points.map((point): Point => {
const newCoordinate = point[dimension] * scale;
const newPoint = [...point];
newPoint[dimension] = newCoordinate;
if (newCoordinate < nextMinCoordinate) {
nextMinCoordinate = newCoordinate;
}
return newPoint as Point;
});
if (!normalize) {
return scaledPoints;
}
if (scaledPoints.length === 2) {
// we don't translate two-point lines
return scaledPoints;
}
const translation = minCoordinate - nextMinCoordinate;
const nextPoints = scaledPoints.map((scaledPoint) =>
pointFromPair<Point>(
scaledPoint.map((value, currentDimension) => {
return currentDimension === dimension ? value + translation : value;
}) as [number, number],
),
);
return nextPoints;
};

View file

@ -8447,10 +8447,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"value": null, "value": null,
}, },
}, },
"pointerOffset": { "pointerOffset": [
"x": 0, 0,
"y": 0, 0,
}, ],
"segmentMidPointHoveredCoords": null, "segmentMidPointHoveredCoords": null,
"selectedPointsIndices": null, "selectedPointsIndices": null,
"startBindingElement": "keep", "startBindingElement": "keep",
@ -8667,10 +8667,10 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"value": null, "value": null,
}, },
}, },
"pointerOffset": { "pointerOffset": [
"x": 0, 0,
"y": 0, 0,
}, ],
"segmentMidPointHoveredCoords": null, "segmentMidPointHoveredCoords": null,
"selectedPointsIndices": null, "selectedPointsIndices": null,
"startBindingElement": "keep", "startBindingElement": "keep",
@ -9077,10 +9077,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"value": null, "value": null,
}, },
}, },
"pointerOffset": { "pointerOffset": [
"x": 0, 0,
"y": 0, 0,
}, ],
"segmentMidPointHoveredCoords": null, "segmentMidPointHoveredCoords": null,
"selectedPointsIndices": null, "selectedPointsIndices": null,
"startBindingElement": "keep", "startBindingElement": "keep",
@ -9474,10 +9474,10 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"value": null, "value": null,
}, },
}, },
"pointerOffset": { "pointerOffset": [
"x": 0, 0,
"y": 0, 0,
}, ],
"segmentMidPointHoveredCoords": null, "segmentMidPointHoveredCoords": null,
"selectedPointsIndices": null, "selectedPointsIndices": null,
"startBindingElement": "keep", "startBindingElement": "keep",

View file

@ -1,5 +1,5 @@
import type { LineSegment } from "../math";
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math"; import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math";
import type { LineSegment } from "../utils";
import type { BoundingBox, Bounds } from "./element/bounds"; import type { BoundingBox, Bounds } from "./element/bounds";
import { isBounds } from "./element/typeChecks"; import { isBounds } from "./element/typeChecks";

View file

@ -1,13 +1,43 @@
import type { import type { Degrees, GenericPoint, PolarCoords, Radians } from "./types";
Degrees,
GlobalPoint,
LocalPoint,
PolarCoords,
Radians,
ViewportPoint,
} from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
/**
* Construct an angle value in radians
*
* @param angle The number to mark as radians
* @returns The radians typed value
*/
export function radians(angle: number): Radians {
return angle as Radians;
}
/**
* Construct an angle value in degrees
*
* @param angle The number to mark as degrees
* @returns The degrees typed value
*/
export function degrees(angle: number): Degrees {
return angle as Degrees;
}
/**
* Construct a polar coordinate
*
* @param radius The radius of the circle to address with this coordinate
* @param angle The angle from the "northest" point of the cirle to address
* @returns The polar coordinate value
*/
export function polar(radius: number, angle: Radians): PolarCoords {
return [radius, angle] as PolarCoords;
}
/**
* Convert an angle in radians into it's smallest octave
*
* @param angle The angle to normalie
* @returns The normalized angle in radians
*/
// TODO: Simplify with modulo and fix for angles beyond 4*Math.PI and - 4*Math.PI // TODO: Simplify with modulo and fix for angles beyond 4*Math.PI and - 4*Math.PI
export const normalizeRadians = (angle: Radians): Radians => { export const normalizeRadians = (angle: Radians): Radians => {
if (angle < 0) { if (angle < 0) {
@ -23,15 +53,31 @@ export const normalizeRadians = (angle: Radians): Radians => {
* Return the polar coordinates for the given cartesian point represented by * Return the polar coordinates for the given cartesian point represented by
* (x, y) for the center point 0,0 where the first number returned is the radius, * (x, y) for the center point 0,0 where the first number returned is the radius,
* the second is the angle in radians. * the second is the angle in radians.
*
* @param param0
* @returns
*/ */
export const cartesian2Polar = < export const cartesian2Polar = <P extends GenericPoint>([
P extends GlobalPoint | LocalPoint | ViewportPoint, x,
>([x, y]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)]; y,
]: P): PolarCoords => polar(Math.hypot(x, y), radians(Math.atan2(y, x)));
/**
* Convert an angle in degrees into randians
*
* @param degrees The angle to convert
* @returns The angle in radians
*/
export function degreesToRadians(degrees: Degrees): Radians { export function degreesToRadians(degrees: Degrees): Radians {
return ((degrees * Math.PI) / 180) as Radians; return ((degrees * Math.PI) / 180) as Radians;
} }
/**
* Convert an angle in radians into degrees
*
* @param degrees The angle to convert
* @returns The angle in degrees
*/
export function radiansToDegrees(degrees: Radians): Degrees { export function radiansToDegrees(degrees: Radians): Degrees {
return ((degrees * 180) / Math.PI) as Degrees; return ((degrees * 180) / Math.PI) as Degrees;
} }

View file

@ -1,15 +1,12 @@
import { isPointOnSymmetricArc } from "./arc"; import { radians } from "./angle";
import { arc, isPointOnSymmetricArc } 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( isPointOnSymmetricArc(
{ arc(1, radians(-Math.PI / 4), radians(Math.PI / 4)),
radius: 1,
startAngle: -Math.PI / 4,
endAngle: Math.PI / 4,
},
point(0.92291667, 0.385), point(0.92291667, 0.385),
), ),
).toBe(true); ).toBe(true);
@ -17,11 +14,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( isPointOnSymmetricArc(
{ arc(1, radians(-Math.PI / 4), radians(Math.PI / 4)),
radius: 1,
startAngle: -Math.PI / 4,
endAngle: Math.PI / 4,
},
point(-0.92291667, 0.385), point(-0.92291667, 0.385),
), ),
).toBe(false); ).toBe(false);
@ -29,11 +22,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( isPointOnSymmetricArc(
{ arc(1, radians(-Math.PI / 4), radians(Math.PI / 4)),
radius: 1,
startAngle: -Math.PI / 4,
endAngle: Math.PI / 4,
},
point(-0.5, 0.5), point(-0.5, 0.5),
), ),
).toBe(false); ).toBe(false);

View file

@ -1,22 +1,29 @@
import { cartesian2Polar } from "./angle"; import { cartesian2Polar } from "./angle";
import type { import type { GenericPoint, Radians, SymmetricArc } from "./types";
GlobalPoint,
LocalPoint,
SymmetricArc,
ViewportPoint,
} from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
/**
* Constructs a symmetric arc defined by the originating circle radius
* the start angle and end angle with 0 radians being the "northest" point
* of the circle.
*
* @param radius The radius of the circle this arc lies on
* @param startAngle The start 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
*/
export function arc(radius: number, startAngle: Radians, endAngle: Radians) {
return { radius, startAngle, endAngle } as SymmetricArc;
}
/** /**
* 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 const isPointOnSymmetricArc = < export function isPointOnSymmetricArc<P extends GenericPoint>(
P extends GlobalPoint | LocalPoint | ViewportPoint,
>(
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc, { radius: arcRadius, startAngle, endAngle }: SymmetricArc,
point: P, point: P,
): boolean => { ): boolean {
const [radius, angle] = cartesian2Polar(point); const [radius, angle] = cartesian2Polar(point);
return startAngle < endAngle return startAngle < endAngle
@ -24,4 +31,4 @@ export const isPointOnSymmetricArc = <
startAngle <= angle && startAngle <= angle &&
endAngle >= angle endAngle >= angle
: startAngle <= angle || endAngle >= angle; : startAngle <= angle || endAngle >= angle;
}; }

View file

@ -1,5 +1,5 @@
import { point, pointRotateRads } from "./point"; import { point, pointRotateRads } from "./point";
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types"; import type { Curve, GenericPoint, Radians } from "./types";
/** /**
* *
@ -9,7 +9,7 @@ import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
* @param d * @param d
* @returns * @returns
*/ */
export function curve<Point extends GlobalPoint | LocalPoint>( export function curve<Point extends GenericPoint>(
a: Point, a: Point,
b: Point, b: Point,
c: Point, c: Point,
@ -18,7 +18,7 @@ export function curve<Point extends GlobalPoint | LocalPoint>(
return [a, b, c, d] as Curve<Point>; return [a, b, c, d] as Curve<Point>;
} }
export const curveRotate = <Point extends LocalPoint | GlobalPoint>( export const curveRotate = <Point extends GenericPoint>(
curve: Curve<Point>, curve: Curve<Point>,
angle: Radians, angle: Radians,
origin: Point, origin: Point,
@ -32,7 +32,7 @@ export const curveRotate = <Point extends LocalPoint | GlobalPoint>(
* @param curveTightness * @param curveTightness
* @returns * @returns
*/ */
export function curveToBezier<Point extends LocalPoint | GlobalPoint>( export function curveToBezier<Point extends GenericPoint>(
pointsIn: readonly Point[], pointsIn: readonly Point[],
curveTightness = 0, curveTightness = 0,
): Point[] { ): Point[] {
@ -84,7 +84,7 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
* @param controlPoints * @param controlPoints
* @returns * @returns
*/ */
export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>( export const cubicBezierPoint = <Point extends GenericPoint>(
t: number, t: number,
controlPoints: Curve<Point>, controlPoints: Curve<Point>,
): Point => { ): Point => {
@ -111,7 +111,7 @@ export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
* @param controlPoints * @param controlPoints
* @returns * @returns
*/ */
export const cubicBezierDistance = <Point extends LocalPoint | GlobalPoint>( export const cubicBezierDistance = <Point extends GenericPoint>(
point: Point, point: Point,
controlPoints: Curve<Point>, controlPoints: Curve<Point>,
) => { ) => {
@ -169,7 +169,7 @@ const solveCubic = (a: number, b: number, c: number, d: number) => {
return roots; return roots;
}; };
const findClosestParameter = <Point extends LocalPoint | GlobalPoint>( const findClosestParameter = <Point extends GenericPoint>(
point: Point, point: Point,
controlPoints: Curve<Point>, controlPoints: Curve<Point>,
) => { ) => {

View file

@ -6,6 +6,8 @@ import type {
Degrees, Degrees,
Vector, Vector,
ViewportPoint, ViewportPoint,
GenericPoint,
Extent,
} from "./types"; } from "./types";
import { PRECISION } from "./utils"; import { PRECISION } from "./utils";
import { vectorFromPoint, vectorScale } from "./vector"; import { vectorFromPoint, vectorScale } from "./vector";
@ -259,3 +261,73 @@ export const isPointWithinBounds = <
q[1] >= Math.min(p[1], r[1]) q[1] >= Math.min(p[1], r[1])
); );
}; };
/**
* The extent (width and height) of a set of points.
*
* @param points The points to calculate the extent for
* @returns
*/
export const pointExtent = (points: readonly GenericPoint[]): Extent => {
const xs = points.map((point) => point[0]);
const ys = points.map((point) => point[1]);
return {
width: Math.max(...xs) - Math.min(...xs),
height: Math.max(...ys) - Math.min(...ys),
} as Extent;
};
/**
* Rescale the set of points from the top leftmost point as origin
*
* @param dimension 0 for rescaling only x, 1 for y
* @param newSize The target size
* @param points The points to restcale
* @param normalize Whether to normalize the result
*/
// TODO: Center should be parametric and should use pointScaleFromOrigin()
export const pointRescaleFromTopLeft = <Point extends GenericPoint>(
dimension: 0 | 1,
newSize: number,
points: readonly Point[],
normalize: boolean,
): Point[] => {
const coordinates = points.map((point) => point[dimension]);
const maxCoordinate = Math.max(...coordinates);
const minCoordinate = Math.min(...coordinates);
const size = maxCoordinate - minCoordinate;
const scale = size === 0 ? 1 : newSize / size;
let nextMinCoordinate = Infinity;
const scaledPoints = points.map((point): Point => {
const newCoordinate = point[dimension] * scale;
const newPoint = [...point];
newPoint[dimension] = newCoordinate;
if (newCoordinate < nextMinCoordinate) {
nextMinCoordinate = newCoordinate;
}
return newPoint as Point;
});
if (!normalize) {
return scaledPoints;
}
if (scaledPoints.length === 2) {
// we don't translate two-point lines
return scaledPoints;
}
const translation = minCoordinate - nextMinCoordinate;
const nextPoints = scaledPoints.map((scaledPoint) =>
pointFromPair<Point>(
scaledPoint.map((value, currentDimension) => {
return currentDimension === dimension ? value + translation : value;
}) as [number, number],
),
);
return nextPoints;
};

View file

@ -87,6 +87,6 @@ export const rangeIncludesValue = (
* @param range The range of which to measure the extent of * @param range The range of which to measure the extent of
* @returns The scalar distance or extent of the start and end of the range * @returns The scalar distance or extent of the start and end of the range
*/ */
export function rangeExtent([a, b]: InclusiveRange) { export function rangeExtent([a, b]: InclusiveRange): number {
return Math.abs(a - b); return Math.abs(a - b);
} }

View file

@ -0,0 +1,28 @@
import { invariant } from "../excalidraw/utils";
import type { GenericPoint, Rectangle } from "./types";
export function rectangle<P extends GenericPoint>(
a: P,
b: P,
c: P,
d: P,
): Rectangle<P> {
return [a, b, c, d] as Rectangle<P>;
}
export function rectangleFromQuad<P extends GenericPoint>(
quad: [a: P, b: P, c: P, d: P],
): Rectangle<P> {
return quad as Rectangle<P>;
}
export function rectangleFromArray<P extends GenericPoint>(
pointArray: P[],
): Rectangle<P> {
invariant(
pointArray.length === 4,
"Point array contains more or less points to create a rectangle from",
);
return pointArray as Rectangle<P>;
}

View file

@ -14,19 +14,11 @@ export type Radians = number & { _brand: "excalimath__radian" };
*/ */
export type Degrees = number & { _brand: "excalimath_degree" }; export type Degrees = number & { _brand: "excalimath_degree" };
//
// Range
//
/** /**
* A number range which includes the start and end numbers in the range. * A number range which includes the start and end numbers in the range.
*/ */
export type InclusiveRange = [number, number] & { _brand: "excalimath_degree" }; export type InclusiveRange = [number, number] & { _brand: "excalimath_degree" };
//
// Point
//
/** /**
* Represents a 2D position in world or canvas space. A * Represents a 2D position in world or canvas space. A
* global coordinate. * global coordinate.
@ -50,7 +42,18 @@ export type ViewportPoint = [x: number, y: number] & {
_brand: "excalimath_viewportpoint"; _brand: "excalimath_viewportpoint";
}; };
// Line /**
* A coordinate system useful for circular path calculations
*/
export type PolarCoords = [radius: number, angle: Radians] & {
_brand: "excalimath_polarCoords";
};
/**
* Aggregate type of all the point types when a function
* is point type agnostic
*/
export type GenericPoint = GlobalPoint | LocalPoint | ViewportPoint;
/** /**
* A line is an infinitely long object with no width, depth, or curvature. * A line is an infinitely long object with no width, depth, or curvature.
@ -71,10 +74,6 @@ export type LineSegment<P extends GlobalPoint | LocalPoint | ViewportPoint> = [
_brand: "excalimath_linesegment"; _brand: "excalimath_linesegment";
}; };
//
// Vector
//
/** /**
* Represents a 2D vector * Represents a 2D vector
*/ */
@ -82,12 +81,10 @@ export type Vector = [u: number, v: number] & {
_brand: "excalimath__vector"; _brand: "excalimath__vector";
}; };
// Triangles
/** /**
* A triangle represented by 3 points * A triangle represented by 3 points
*/ */
export type Triangle<P extends GlobalPoint | LocalPoint> = [ export type Triangle<P extends GlobalPoint | LocalPoint | ViewportPoint> = [
a: P, a: P,
b: P, b: P,
c: P, c: P,
@ -95,9 +92,17 @@ export type Triangle<P extends GlobalPoint | LocalPoint> = [
_brand: "excalimath__triangle"; _brand: "excalimath__triangle";
}; };
// /**
// Polygon * A rectangular shape represented by 4 points at its corners
// */
export type Rectangle<P extends GlobalPoint | LocalPoint | ViewportPoint> = [
a: P,
b: P,
c: P,
d: P,
] & {
_brand: "excalimath__rectangle";
};
/** /**
* A polygon is a closed shape by connecting the given points * A polygon is a closed shape by connecting the given points
@ -108,10 +113,6 @@ export type Polygon<Point extends GlobalPoint | LocalPoint | ViewportPoint> =
_brand: "excalimath_polygon"; _brand: "excalimath_polygon";
}; };
//
// Curve
//
/** /**
* Cubic bezier curve with four control points * Cubic bezier curve with four control points
*/ */
@ -124,18 +125,24 @@ export type Curve<Point extends GlobalPoint | LocalPoint | ViewportPoint> = [
_brand: "excalimath_curve"; _brand: "excalimath_curve";
}; };
export type PolarCoords = [
radius: number,
/** angle in radians */
angle: number,
];
/** /**
* 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 = { export type SymmetricArc = {
radius: number; radius: number;
startAngle: number; startAngle: Radians;
endAngle: number; endAngle: Radians;
} & {
_brand: "excalimath_symmetricarc";
};
/**
* The width and height represented as a type
*/
export type Extent = {
width: number;
height: number;
} & {
_brand: "excalimath_extent";
}; };

View file

@ -1,3 +1,4 @@
import type { LineSegment } from "../math";
import { import {
vectorCross, vectorCross,
vectorFromPoint, vectorFromPoint,
@ -6,8 +7,6 @@ import {
} from "../math"; } from "../math";
import type { Bounds } from "../excalidraw/element/bounds"; import type { Bounds } from "../excalidraw/element/bounds";
export type LineSegment<P extends LocalPoint | GlobalPoint> = [P, P];
export function getBBox<P extends LocalPoint | GlobalPoint>( export function getBBox<P extends LocalPoint | GlobalPoint>(
line: LineSegment<P>, line: LineSegment<P>,
): Bounds { ): Bounds {