mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Further math refactor and simplifications
This commit is contained in:
parent
41885b4bb3
commit
0e2f8c958e
18 changed files with 262 additions and 175 deletions
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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",
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
};
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
) => {
|
) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
28
packages/math/rectangle.ts
Normal file
28
packages/math/rectangle.ts
Normal 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>;
|
||||||
|
}
|
|
@ -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";
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue