feat: Remove GA code from binding (#9042)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
31e8476c78
commit
0ffeaeaecf
@ -12,11 +12,13 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "../../packages/excalidraw/components/icons";
|
} from "../../packages/excalidraw/components/icons";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
import type { Curve } from "../../packages/math";
|
||||||
import {
|
import {
|
||||||
isLineSegment,
|
isLineSegment,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LineSegment,
|
type LineSegment,
|
||||||
} from "../../packages/math";
|
} from "../../packages/math";
|
||||||
|
import { isCurve } from "../../packages/math/curve";
|
||||||
|
|
||||||
const renderLine = (
|
const renderLine = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@ -33,6 +35,28 @@ const renderLine = (
|
|||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderCubicBezier = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
zoom: number,
|
||||||
|
[start, control1, control2, end]: Curve<GlobalPoint>,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(start[0] * zoom, start[1] * zoom);
|
||||||
|
context.bezierCurveTo(
|
||||||
|
control1[0] * zoom,
|
||||||
|
control1[1] * zoom,
|
||||||
|
control2[0] * zoom,
|
||||||
|
control2[1] * zoom,
|
||||||
|
end[0] * zoom,
|
||||||
|
end[1] * zoom,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
||||||
context.strokeStyle = "#888";
|
context.strokeStyle = "#888";
|
||||||
context.save();
|
context.save();
|
||||||
@ -60,6 +84,16 @@ const render = (
|
|||||||
el.color,
|
el.color,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case isCurve(el.data):
|
||||||
|
renderCubicBezier(
|
||||||
|
context,
|
||||||
|
appState.zoom.value,
|
||||||
|
el.data as Curve<GlobalPoint>,
|
||||||
|
el.color,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown element type ${JSON.stringify(el)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1633,18 +1633,16 @@ export const actionChangeArrowType = register({
|
|||||||
|
|
||||||
const finalStartPoint = startHoveredElement
|
const finalStartPoint = startHoveredElement
|
||||||
? bindPointToSnapToElementOutline(
|
? bindPointToSnapToElementOutline(
|
||||||
startGlobalPoint,
|
newElement,
|
||||||
endGlobalPoint,
|
|
||||||
startHoveredElement,
|
startHoveredElement,
|
||||||
elementsMap,
|
"start",
|
||||||
)
|
)
|
||||||
: startGlobalPoint;
|
: startGlobalPoint;
|
||||||
const finalEndPoint = endHoveredElement
|
const finalEndPoint = endHoveredElement
|
||||||
? bindPointToSnapToElementOutline(
|
? bindPointToSnapToElementOutline(
|
||||||
endGlobalPoint,
|
newElement,
|
||||||
startGlobalPoint,
|
|
||||||
endHoveredElement,
|
endHoveredElement,
|
||||||
elementsMap,
|
"end",
|
||||||
)
|
)
|
||||||
: endGlobalPoint;
|
: endGlobalPoint;
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
|||||||
export const SIDE_RESIZING_THRESHOLD = 2 * DEFAULT_TRANSFORM_HANDLE_SPACING;
|
export const SIDE_RESIZING_THRESHOLD = 2 * DEFAULT_TRANSFORM_HANDLE_SPACING;
|
||||||
// a small epsilon to make side resizing always take precedence
|
// a small epsilon to make side resizing always take precedence
|
||||||
// (avoids an increase in renders and changes to tests)
|
// (avoids an increase in renders and changes to tests)
|
||||||
const EPSILON = 0.00001;
|
export const EPSILON = 0.00001;
|
||||||
export const DEFAULT_COLLISION_THRESHOLD =
|
export const DEFAULT_COLLISION_THRESHOLD =
|
||||||
2 * SIDE_RESIZING_THRESHOLD - EPSILON;
|
2 * SIDE_RESIZING_THRESHOLD - EPSILON;
|
||||||
|
|
||||||
|
@ -88,9 +88,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
"fixedPoint": null,
|
"focus": -0.007519379844961235,
|
||||||
"focus": -0.008153707962747813,
|
"gap": 11.562288374879595,
|
||||||
"gap": 1,
|
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -119,8 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id49",
|
"elementId": "id49",
|
||||||
"fixedPoint": null,
|
"focus": -0.0813953488372095,
|
||||||
"focus": -0.08139534883720931,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1864ab",
|
"strokeColor": "#1864ab",
|
||||||
@ -146,9 +144,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0.10666666666666667,
|
"focus": 0.10666666666666667,
|
||||||
"gap": 3.834326468444573,
|
"gap": 3.8343264684446097,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -177,9 +174,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "diamond-1",
|
"elementId": "diamond-1",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 4.545343408287929,
|
||||||
},
|
},
|
||||||
"strokeColor": "#e67700",
|
"strokeColor": "#e67700",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -338,7 +334,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 14,
|
"gap": 14,
|
||||||
},
|
},
|
||||||
@ -369,7 +364,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "text-1",
|
"elementId": "text-1",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -442,8 +436,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id42",
|
"elementId": "id42",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -473,7 +466,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id41",
|
"elementId": "id41",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -620,8 +612,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id46",
|
"elementId": "id46",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -651,7 +642,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id45",
|
"elementId": "id45",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -1484,8 +1474,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "Alice",
|
"elementId": "Alice",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 5.299874999999986,
|
"gap": 5.299874999999986,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -1517,7 +1506,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "Bob",
|
"elementId": "Bob",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -1549,9 +1537,8 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "B",
|
"elementId": "B",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 14,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -1578,7 +1565,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "Bob",
|
"elementId": "Bob",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
|
@ -434,7 +434,7 @@ describe("Test Transform", () => {
|
|||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: ellipse.id,
|
elementId: ellipse.id,
|
||||||
focus: 0,
|
focus: -0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -519,7 +519,7 @@ describe("Test Transform", () => {
|
|||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: text3.id,
|
elementId: text3.id,
|
||||||
focus: 0,
|
focus: -0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -777,8 +777,7 @@ describe("Test Transform", () => {
|
|||||||
const [arrow, rect] = excalidrawElements;
|
const [arrow, rect] = excalidrawElements;
|
||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
fixedPoint: null,
|
focus: -0,
|
||||||
focus: 0,
|
|
||||||
gap: 14,
|
gap: 14,
|
||||||
});
|
});
|
||||||
expect(rect.boundElements).toStrictEqual([
|
expect(rect.boundElements).toStrictEqual([
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getElementBounds } from "./bounds";
|
import { getElementBounds } from "./bounds";
|
||||||
import type { FrameNameBounds } from "../types";
|
import type { FrameNameBounds } from "../types";
|
||||||
@ -16,8 +19,28 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { getBoundTextShape, isPathALoop } from "../shapes";
|
import { getBoundTextShape, isPathALoop } from "../shapes";
|
||||||
import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
|
import type {
|
||||||
import { isPointWithinBounds, pointFrom } from "../../math";
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
LocalPoint,
|
||||||
|
Polygon,
|
||||||
|
Radians,
|
||||||
|
} from "../../math";
|
||||||
|
import {
|
||||||
|
curveIntersectLineSegment,
|
||||||
|
isPointWithinBounds,
|
||||||
|
line,
|
||||||
|
lineSegment,
|
||||||
|
lineSegmentIntersectionPoints,
|
||||||
|
pointFrom,
|
||||||
|
pointRotateRads,
|
||||||
|
pointsEqual,
|
||||||
|
} from "../../math";
|
||||||
|
import { ellipse, ellipseLineIntersectionPoints } from "../../math/ellipse";
|
||||||
|
import {
|
||||||
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
@ -121,3 +144,166 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
return !!textShape && isPointInShape(pointFrom(x, y), textShape);
|
return !!textShape && isPointInShape(pointFrom(x, y), textShape);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intersect a line with an element for binding test
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
* @param line
|
||||||
|
* @param offset
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const intersectElementWithLineSegment = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
line: LineSegment<GlobalPoint>,
|
||||||
|
offset: number = 0,
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
switch (element.type) {
|
||||||
|
case "rectangle":
|
||||||
|
case "image":
|
||||||
|
case "text":
|
||||||
|
case "iframe":
|
||||||
|
case "embeddable":
|
||||||
|
case "frame":
|
||||||
|
case "magicframe":
|
||||||
|
return intersectRectanguloidWithLineSegment(element, line, offset);
|
||||||
|
case "diamond":
|
||||||
|
return intersectDiamondWithLineSegment(element, line, offset);
|
||||||
|
case "ellipse":
|
||||||
|
return intersectEllipseWithLineSegment(element, line, offset);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unimplemented element type '${element.type}'`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const intersectRectanguloidWithLineSegment = (
|
||||||
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
l: LineSegment<GlobalPoint>,
|
||||||
|
offset: number = 0,
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
|
// instead. It's all the same distance-wise.
|
||||||
|
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||||
|
l[0],
|
||||||
|
center,
|
||||||
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
|
const rotatedB = pointRotateRads<GlobalPoint>(
|
||||||
|
l[1],
|
||||||
|
center,
|
||||||
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the element's building components we can test against
|
||||||
|
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
||||||
|
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
// Test intersection against the sides, keep only the valid
|
||||||
|
// intersection points and rotate them back to scene space
|
||||||
|
...sides
|
||||||
|
.map((s) =>
|
||||||
|
lineSegmentIntersectionPoints(
|
||||||
|
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||||
|
s,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter((x) => x != null)
|
||||||
|
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)),
|
||||||
|
// Test intersection against the corners which are cubic bezier curves,
|
||||||
|
// keep only the valid intersection points and rotate them back to scene
|
||||||
|
// space
|
||||||
|
...corners
|
||||||
|
.flatMap((t) =>
|
||||||
|
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
||||||
|
)
|
||||||
|
.filter((i) => i != null)
|
||||||
|
.map((j) => pointRotateRads(j, center, element.angle)),
|
||||||
|
]
|
||||||
|
// Remove duplicates
|
||||||
|
.filter(
|
||||||
|
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const intersectDiamondWithLineSegment = (
|
||||||
|
element: ExcalidrawDiamondElement,
|
||||||
|
l: LineSegment<GlobalPoint>,
|
||||||
|
offset: number = 0,
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
|
// points. It's all the same distance-wise.
|
||||||
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
||||||
|
const [sides, curves] = deconstructDiamondElement(element, offset);
|
||||||
|
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
...sides
|
||||||
|
.map((s) =>
|
||||||
|
lineSegmentIntersectionPoints(
|
||||||
|
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||||
|
s,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter((p): p is GlobalPoint => p != null)
|
||||||
|
// Rotate back intersection points
|
||||||
|
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)),
|
||||||
|
...curves
|
||||||
|
.flatMap((p) =>
|
||||||
|
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
||||||
|
)
|
||||||
|
.filter((p) => p != null)
|
||||||
|
// Rotate back intersection points
|
||||||
|
.map((p) => pointRotateRads(p, center, element.angle)),
|
||||||
|
]
|
||||||
|
// Remove duplicates
|
||||||
|
.filter(
|
||||||
|
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const intersectEllipseWithLineSegment = (
|
||||||
|
element: ExcalidrawEllipseElement,
|
||||||
|
l: LineSegment<GlobalPoint>,
|
||||||
|
offset: number = 0,
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
||||||
|
return ellipseLineIntersectionPoints(
|
||||||
|
ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
|
||||||
|
line(rotatedA, rotatedB),
|
||||||
|
).map((p) => pointRotateRads(p, center, element.angle));
|
||||||
|
};
|
||||||
|
123
packages/excalidraw/element/distance.ts
Normal file
123
packages/excalidraw/element/distance.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import type { GlobalPoint, Radians } from "../../math";
|
||||||
|
import {
|
||||||
|
curvePointDistance,
|
||||||
|
distanceToLineSegment,
|
||||||
|
pointFrom,
|
||||||
|
pointRotateRads,
|
||||||
|
} from "../../math";
|
||||||
|
import { ellipse, ellipseDistanceFromPoint } from "../../math/ellipse";
|
||||||
|
import type {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
} from "./types";
|
||||||
|
import {
|
||||||
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
export const distanceToBindableElement = (
|
||||||
|
element: ExcalidrawBindableElement,
|
||||||
|
p: GlobalPoint,
|
||||||
|
): number => {
|
||||||
|
switch (element.type) {
|
||||||
|
case "rectangle":
|
||||||
|
case "image":
|
||||||
|
case "text":
|
||||||
|
case "iframe":
|
||||||
|
case "embeddable":
|
||||||
|
case "frame":
|
||||||
|
case "magicframe":
|
||||||
|
return distanceToRectanguloidElement(element, p);
|
||||||
|
case "diamond":
|
||||||
|
return distanceToDiamondElement(element, p);
|
||||||
|
case "ellipse":
|
||||||
|
return distanceToEllipseElement(element, p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the distance of a point and the provided rectangular-shaped element,
|
||||||
|
* accounting for roundness and rotation
|
||||||
|
*
|
||||||
|
* @param element The rectanguloid element
|
||||||
|
* @param p The point to consider
|
||||||
|
* @returns The eucledian distance to the outline of the rectanguloid element
|
||||||
|
*/
|
||||||
|
const distanceToRectanguloidElement = (
|
||||||
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
p: GlobalPoint,
|
||||||
|
) => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
|
// instead. It's all the same distance-wise.
|
||||||
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
|
// Get the element's building components we can test against
|
||||||
|
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
||||||
|
...corners
|
||||||
|
.map((a) => curvePointDistance(a, rotatedPoint))
|
||||||
|
.filter((d): d is number => d !== null),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the distance of a point and the provided diamond element, accounting
|
||||||
|
* for roundness and rotation
|
||||||
|
*
|
||||||
|
* @param element The diamond element
|
||||||
|
* @param p The point to consider
|
||||||
|
* @returns The eucledian distance to the outline of the diamond
|
||||||
|
*/
|
||||||
|
const distanceToDiamondElement = (
|
||||||
|
element: ExcalidrawDiamondElement,
|
||||||
|
p: GlobalPoint,
|
||||||
|
): number => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
|
// points. It's all the same distance-wise.
|
||||||
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
|
const [sides, curves] = deconstructDiamondElement(element);
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
||||||
|
...curves
|
||||||
|
.map((a) => curvePointDistance(a, rotatedPoint))
|
||||||
|
.filter((d): d is number => d !== null),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the distance of a point and the provided ellipse element, accounting
|
||||||
|
* for roundness and rotation
|
||||||
|
*
|
||||||
|
* @param element The ellipse element
|
||||||
|
* @param p The point to consider
|
||||||
|
* @returns The eucledian distance to the outline of the ellipse
|
||||||
|
*/
|
||||||
|
const distanceToEllipseElement = (
|
||||||
|
element: ExcalidrawEllipseElement,
|
||||||
|
p: GlobalPoint,
|
||||||
|
): number => {
|
||||||
|
const center = pointFrom(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
return ellipseDistanceFromPoint(
|
||||||
|
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||||
|
pointRotateRads(p, center, -element.angle as Radians),
|
||||||
|
ellipse(center, element.width / 2, element.height / 2),
|
||||||
|
);
|
||||||
|
};
|
@ -18,6 +18,7 @@ import type {
|
|||||||
import { ARROW_TYPE } from "../constants";
|
import { ARROW_TYPE } from "../constants";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { pointFrom } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
import "../../utils/test-utils";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -19,8 +19,6 @@ import { invariant, isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
|
|||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import {
|
import {
|
||||||
bindPointToSnapToElementOutline,
|
bindPointToSnapToElementOutline,
|
||||||
distanceToBindableElement,
|
|
||||||
avoidRectangularCorner,
|
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
getHeadingForElbowArrowSnap,
|
getHeadingForElbowArrowSnap,
|
||||||
getGlobalFixedPointForBindableElement,
|
getGlobalFixedPointForBindableElement,
|
||||||
@ -42,7 +40,7 @@ import {
|
|||||||
headingForPoint,
|
headingForPoint,
|
||||||
} from "./heading";
|
} from "./heading";
|
||||||
import { type ElementUpdate } from "./mutateElement";
|
import { type ElementUpdate } from "./mutateElement";
|
||||||
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
import { isBindableElement } from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
type ExcalidrawElbowArrowElement,
|
type ExcalidrawElbowArrowElement,
|
||||||
type NonDeletedSceneElementsMap,
|
type NonDeletedSceneElementsMap,
|
||||||
@ -55,6 +53,7 @@ import type {
|
|||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { distanceToBindableElement } from "./distance";
|
||||||
|
|
||||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||||
|
|
||||||
@ -1177,19 +1176,27 @@ const getElbowArrowData = (
|
|||||||
)
|
)
|
||||||
: [startElement, endElement];
|
: [startElement, endElement];
|
||||||
const startGlobalPoint = getGlobalPoint(
|
const startGlobalPoint = getGlobalPoint(
|
||||||
|
{
|
||||||
|
...arrow,
|
||||||
|
elbowed: true,
|
||||||
|
points: nextPoints,
|
||||||
|
} as ExcalidrawElbowArrowElement,
|
||||||
|
"start",
|
||||||
arrow.startBinding?.fixedPoint,
|
arrow.startBinding?.fixedPoint,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
origEndGlobalPoint,
|
|
||||||
elementsMap,
|
|
||||||
startElement,
|
startElement,
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
const endGlobalPoint = getGlobalPoint(
|
const endGlobalPoint = getGlobalPoint(
|
||||||
|
{
|
||||||
|
...arrow,
|
||||||
|
elbowed: true,
|
||||||
|
points: nextPoints,
|
||||||
|
} as ExcalidrawElbowArrowElement,
|
||||||
|
"end",
|
||||||
arrow.endBinding?.fixedPoint,
|
arrow.endBinding?.fixedPoint,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
origStartGlobalPoint,
|
|
||||||
elementsMap,
|
|
||||||
endElement,
|
endElement,
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
@ -2133,21 +2140,20 @@ const neighborIndexToHeading = (idx: number): Heading => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getGlobalPoint = (
|
const getGlobalPoint = (
|
||||||
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
|
startOrEnd: "start" | "end",
|
||||||
fixedPointRatio: [number, number] | undefined | null,
|
fixedPointRatio: [number, number] | undefined | null,
|
||||||
initialPoint: GlobalPoint,
|
initialPoint: GlobalPoint,
|
||||||
otherPoint: GlobalPoint,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
boundElement?: ExcalidrawBindableElement | null,
|
boundElement?: ExcalidrawBindableElement | null,
|
||||||
hoveredElement?: ExcalidrawBindableElement | null,
|
hoveredElement?: ExcalidrawBindableElement | null,
|
||||||
isDragging?: boolean,
|
isDragging?: boolean,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
if (hoveredElement) {
|
if (hoveredElement) {
|
||||||
const snapPoint = getSnapPoint(
|
const snapPoint = bindPointToSnapToElementOutline(
|
||||||
initialPoint,
|
arrow,
|
||||||
otherPoint,
|
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
elementsMap,
|
startOrEnd,
|
||||||
);
|
);
|
||||||
|
|
||||||
return snapToMid(hoveredElement, snapPoint);
|
return snapToMid(hoveredElement, snapPoint);
|
||||||
@ -2164,29 +2170,16 @@ const getGlobalPoint = (
|
|||||||
|
|
||||||
// NOTE: Resize scales the binding position point too, so we need to update it
|
// NOTE: Resize scales the binding position point too, so we need to update it
|
||||||
return Math.abs(
|
return Math.abs(
|
||||||
distanceToBindableElement(boundElement, fixedGlobalPoint, elementsMap) -
|
distanceToBindableElement(boundElement, fixedGlobalPoint) -
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
) > 0.01
|
) > 0.01
|
||||||
? getSnapPoint(initialPoint, otherPoint, boundElement, elementsMap)
|
? bindPointToSnapToElementOutline(arrow, boundElement, startOrEnd)
|
||||||
: fixedGlobalPoint;
|
: fixedGlobalPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
return initialPoint;
|
return initialPoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSnapPoint = (
|
|
||||||
p: GlobalPoint,
|
|
||||||
otherPoint: GlobalPoint,
|
|
||||||
element: ExcalidrawBindableElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
) =>
|
|
||||||
bindPointToSnapToElementOutline(
|
|
||||||
isRectanguloidElement(element) ? avoidRectangularCorner(element, p) : p,
|
|
||||||
otherPoint,
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const getBindPointHeading = (
|
const getBindPointHeading = (
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
otherPoint: GlobalPoint,
|
otherPoint: GlobalPoint,
|
||||||
@ -2201,9 +2194,12 @@ const getBindPointHeading = (
|
|||||||
hoveredElement &&
|
hoveredElement &&
|
||||||
aabbForElement(
|
aabbForElement(
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
Array(4).fill(
|
Array(4).fill(distanceToBindableElement(hoveredElement, p)) as [
|
||||||
distanceToBindableElement(hoveredElement, p, elementsMap),
|
number,
|
||||||
) as [number, number, number, number],
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
origPoint,
|
origPoint,
|
||||||
|
355
packages/excalidraw/element/utils.ts
Normal file
355
packages/excalidraw/element/utils.ts
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
import { getDiamondPoints } from ".";
|
||||||
|
import type { Curve, LineSegment } from "../../math";
|
||||||
|
import {
|
||||||
|
curve,
|
||||||
|
lineSegment,
|
||||||
|
pointFrom,
|
||||||
|
pointFromVector,
|
||||||
|
rectangle,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorNormalize,
|
||||||
|
vectorScale,
|
||||||
|
type GlobalPoint,
|
||||||
|
} from "../../math";
|
||||||
|
import { getCornerRadius } from "../shapes";
|
||||||
|
import type {
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the building components of a rectanguloid element in the form of
|
||||||
|
* line segments and curves.
|
||||||
|
*
|
||||||
|
* @param element Target rectanguloid element
|
||||||
|
* @param offset Optional offset to expand the rectanguloid shape
|
||||||
|
* @returns Tuple of line segments (0) and curves (1)
|
||||||
|
*/
|
||||||
|
export function deconstructRectanguloidElement(
|
||||||
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
offset: number = 0,
|
||||||
|
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||||
|
const roundness = getCornerRadius(
|
||||||
|
Math.min(element.width, element.height),
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (roundness <= 0) {
|
||||||
|
const r = rectangle(
|
||||||
|
pointFrom(element.x - offset, element.y - offset),
|
||||||
|
pointFrom(
|
||||||
|
element.x + element.width + offset,
|
||||||
|
element.y + element.height + offset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const top = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[0][0] + roundness, r[0][1]),
|
||||||
|
pointFrom<GlobalPoint>(r[1][0] - roundness, r[0][1]),
|
||||||
|
);
|
||||||
|
const right = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[1][0], r[0][1] + roundness),
|
||||||
|
pointFrom<GlobalPoint>(r[1][0], r[1][1] - roundness),
|
||||||
|
);
|
||||||
|
const bottom = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[0][0] + roundness, r[1][1]),
|
||||||
|
pointFrom<GlobalPoint>(r[1][0] - roundness, r[1][1]),
|
||||||
|
);
|
||||||
|
const left = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[0][0], r[1][1] - roundness),
|
||||||
|
pointFrom<GlobalPoint>(r[0][0], r[0][1] + roundness),
|
||||||
|
);
|
||||||
|
const sides = [top, right, bottom, left];
|
||||||
|
|
||||||
|
return [sides, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const r = rectangle(
|
||||||
|
pointFrom(element.x, element.y),
|
||||||
|
pointFrom(element.x + element.width, element.y + element.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
const top = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[0][0] + roundness, r[0][1]),
|
||||||
|
pointFrom<GlobalPoint>(r[1][0] - roundness, r[0][1]),
|
||||||
|
);
|
||||||
|
const right = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[1][0], r[0][1] + roundness),
|
||||||
|
pointFrom<GlobalPoint>(r[1][0], r[1][1] - roundness),
|
||||||
|
);
|
||||||
|
const bottom = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[0][0] + roundness, r[1][1]),
|
||||||
|
pointFrom<GlobalPoint>(r[1][0] - roundness, r[1][1]),
|
||||||
|
);
|
||||||
|
const left = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(r[0][0], r[1][1] - roundness),
|
||||||
|
pointFrom<GlobalPoint>(r[0][0], r[0][1] + roundness),
|
||||||
|
);
|
||||||
|
|
||||||
|
const offsets = [
|
||||||
|
vectorScale(
|
||||||
|
vectorNormalize(
|
||||||
|
vectorFromPoint(pointFrom(r[0][0] - offset, r[0][1] - offset), center),
|
||||||
|
),
|
||||||
|
offset,
|
||||||
|
), // TOP LEFT
|
||||||
|
vectorScale(
|
||||||
|
vectorNormalize(
|
||||||
|
vectorFromPoint(pointFrom(r[1][0] + offset, r[0][1] - offset), center),
|
||||||
|
),
|
||||||
|
offset,
|
||||||
|
), //TOP RIGHT
|
||||||
|
vectorScale(
|
||||||
|
vectorNormalize(
|
||||||
|
vectorFromPoint(pointFrom(r[1][0] + offset, r[1][1] + offset), center),
|
||||||
|
),
|
||||||
|
offset,
|
||||||
|
), // BOTTOM RIGHT
|
||||||
|
vectorScale(
|
||||||
|
vectorNormalize(
|
||||||
|
vectorFromPoint(pointFrom(r[0][0] - offset, r[1][1] + offset), center),
|
||||||
|
),
|
||||||
|
offset,
|
||||||
|
), // BOTTOM LEFT
|
||||||
|
];
|
||||||
|
|
||||||
|
const corners = [
|
||||||
|
curve(
|
||||||
|
pointFromVector(offsets[0], left[1]),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[0],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
||||||
|
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[0],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
||||||
|
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[0], top[0]),
|
||||||
|
), // TOP LEFT
|
||||||
|
curve(
|
||||||
|
pointFromVector(offsets[1], top[1]),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[1],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
||||||
|
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[1],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
||||||
|
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[1], right[0]),
|
||||||
|
), // TOP RIGHT
|
||||||
|
curve(
|
||||||
|
pointFromVector(offsets[2], right[1]),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[2],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
||||||
|
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[2],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
||||||
|
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[2], bottom[1]),
|
||||||
|
), // BOTTOM RIGHT
|
||||||
|
curve(
|
||||||
|
pointFromVector(offsets[3], bottom[0]),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[3],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
||||||
|
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[3],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
||||||
|
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[3], left[0]),
|
||||||
|
), // BOTTOM LEFT
|
||||||
|
];
|
||||||
|
|
||||||
|
const sides = [
|
||||||
|
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
||||||
|
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
||||||
|
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
||||||
|
lineSegment<GlobalPoint>(corners[3][3], corners[0][0]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [sides, corners];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the building components of a diamond element in the form of
|
||||||
|
* line segments and curves as a tuple, in this order.
|
||||||
|
*
|
||||||
|
* @param element The element to deconstruct
|
||||||
|
* @param offset An optional offset
|
||||||
|
* @returns Tuple of line segments (0) and curves (1)
|
||||||
|
*/
|
||||||
|
export function deconstructDiamondElement(
|
||||||
|
element: ExcalidrawDiamondElement,
|
||||||
|
offset: number = 0,
|
||||||
|
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||||
|
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||||
|
getDiamondPoints(element);
|
||||||
|
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
||||||
|
const horizontalRadius = getCornerRadius(Math.abs(rightY - topY), element);
|
||||||
|
|
||||||
|
if (element.roundness?.type == null) {
|
||||||
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
|
pointFrom(element.x + topX, element.y + topY - offset),
|
||||||
|
pointFrom(element.x + rightX + offset, element.y + rightY),
|
||||||
|
pointFrom(element.x + bottomX, element.y + bottomY + offset),
|
||||||
|
pointFrom(element.x + leftX - offset, element.y + leftY),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create the line segment parts of the diamond
|
||||||
|
// NOTE: Horizontal and vertical seems to be flipped here
|
||||||
|
const topRight = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom(top[0] + verticalRadius, top[1] + horizontalRadius),
|
||||||
|
pointFrom(right[0] - verticalRadius, right[1] - horizontalRadius),
|
||||||
|
);
|
||||||
|
const bottomRight = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom(right[0] - verticalRadius, right[1] + horizontalRadius),
|
||||||
|
pointFrom(bottom[0] + verticalRadius, bottom[1] - horizontalRadius),
|
||||||
|
);
|
||||||
|
const bottomLeft = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom(bottom[0] - verticalRadius, bottom[1] - horizontalRadius),
|
||||||
|
pointFrom(left[0] + verticalRadius, left[1] + horizontalRadius),
|
||||||
|
);
|
||||||
|
const topLeft = lineSegment<GlobalPoint>(
|
||||||
|
pointFrom(left[0] + verticalRadius, left[1] - horizontalRadius),
|
||||||
|
pointFrom(top[0] - verticalRadius, top[1] + horizontalRadius),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
|
pointFrom(element.x + topX, element.y + topY),
|
||||||
|
pointFrom(element.x + rightX, element.y + rightY),
|
||||||
|
pointFrom(element.x + bottomX, element.y + bottomY),
|
||||||
|
pointFrom(element.x + leftX, element.y + leftY),
|
||||||
|
];
|
||||||
|
|
||||||
|
const offsets = [
|
||||||
|
vectorScale(vectorNormalize(vectorFromPoint(right, center)), offset), // RIGHT
|
||||||
|
vectorScale(vectorNormalize(vectorFromPoint(bottom, center)), offset), // BOTTOM
|
||||||
|
vectorScale(vectorNormalize(vectorFromPoint(left, center)), offset), // LEFT
|
||||||
|
vectorScale(vectorNormalize(vectorFromPoint(top, center)), offset), // TOP
|
||||||
|
];
|
||||||
|
|
||||||
|
const corners = [
|
||||||
|
curve(
|
||||||
|
pointFromVector(
|
||||||
|
offsets[0],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
right[0] - verticalRadius,
|
||||||
|
right[1] - horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[0], right),
|
||||||
|
pointFromVector(offsets[0], right),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[0],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
right[0] - verticalRadius,
|
||||||
|
right[1] + horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), // RIGHT
|
||||||
|
curve(
|
||||||
|
pointFromVector(
|
||||||
|
offsets[1],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
bottom[0] + verticalRadius,
|
||||||
|
bottom[1] - horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[1], bottom),
|
||||||
|
pointFromVector(offsets[1], bottom),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[1],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
bottom[0] - verticalRadius,
|
||||||
|
bottom[1] - horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), // BOTTOM
|
||||||
|
curve(
|
||||||
|
pointFromVector(
|
||||||
|
offsets[2],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
left[0] + verticalRadius,
|
||||||
|
left[1] + horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[2], left),
|
||||||
|
pointFromVector(offsets[2], left),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[2],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
left[0] + verticalRadius,
|
||||||
|
left[1] - horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), // LEFT
|
||||||
|
curve(
|
||||||
|
pointFromVector(
|
||||||
|
offsets[3],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
top[0] - verticalRadius,
|
||||||
|
top[1] + horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pointFromVector(offsets[3], top),
|
||||||
|
pointFromVector(offsets[3], top),
|
||||||
|
pointFromVector(
|
||||||
|
offsets[3],
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
top[0] + verticalRadius,
|
||||||
|
top[1] + horizontalRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), // TOP
|
||||||
|
];
|
||||||
|
|
||||||
|
const sides = [
|
||||||
|
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
||||||
|
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
||||||
|
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
||||||
|
lineSegment<GlobalPoint>(corners[3][3], corners[0][0]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [sides, corners];
|
||||||
|
}
|
@ -197,7 +197,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 99,
|
"height": "102.35417",
|
||||||
"id": "id172",
|
"id": "id172",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -211,8 +211,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.20800",
|
"101.77517",
|
||||||
99,
|
"102.35417",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -227,8 +227,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 40,
|
"version": 40,
|
||||||
"width": "98.20800",
|
"width": "101.77517",
|
||||||
"x": 1,
|
"x": "0.70711",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -294,24 +294,22 @@ History {
|
|||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id171",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "0.00990",
|
"focus": "0.00990",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.98017",
|
"height": "0.98597",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
98,
|
"98.58579",
|
||||||
"-0.98017",
|
"-0.98597",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id170",
|
"elementId": "id170",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "0.02970",
|
"focus": "0.02970",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -319,24 +317,22 @@ History {
|
|||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id171",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "-0.02000",
|
"focus": "-0.02000",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.00169",
|
"height": "0.00119",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
98,
|
"98.58579",
|
||||||
"0.00169",
|
"0.00119",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id170",
|
"elementId": "id170",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "0.02000",
|
"focus": "0.02000",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -393,15 +389,15 @@ History {
|
|||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": 99,
|
"height": "102.35417",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.20800",
|
"101.77517",
|
||||||
99,
|
"102.35417",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
@ -410,28 +406,26 @@ History {
|
|||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id171",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "0.00990",
|
"focus": "0.00990",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.98161",
|
"height": "0.98700",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
98,
|
"98.58579",
|
||||||
"-0.98161",
|
"-0.98700",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id170",
|
"elementId": "id170",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "0.02970",
|
"focus": "0.02970",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"y": "0.99245",
|
"y": "0.99465",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id175" => Delta {
|
"id175" => Delta {
|
||||||
@ -824,7 +818,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 30,
|
"version": 30,
|
||||||
"width": 0,
|
"width": 50,
|
||||||
"x": 200,
|
"x": 200,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
@ -858,7 +852,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0,
|
50,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -934,8 +928,7 @@ History {
|
|||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id166",
|
"elementId": "id166",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"points": [
|
"points": [
|
||||||
@ -944,13 +937,12 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0,
|
50,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id165",
|
"elementId": "id165",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -1246,7 +1238,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "2.61991",
|
"height": "2.52823",
|
||||||
"id": "id178",
|
"id": "id178",
|
||||||
"index": "Zz",
|
"index": "Zz",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -1260,8 +1252,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"98.58579",
|
||||||
"-2.61991",
|
"-2.52823",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -1284,9 +1276,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"width": "98.00000",
|
"width": "98.58579",
|
||||||
"x": "1.00000",
|
"x": "0.70711",
|
||||||
"y": "3.98333",
|
"y": "3.82861",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -1617,7 +1609,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "2.61991",
|
"height": "2.52823",
|
||||||
"id": "id181",
|
"id": "id181",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -1631,8 +1623,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"98.58579",
|
||||||
"-2.61991",
|
"-2.52823",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -1655,9 +1647,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"width": "98.00000",
|
"width": "98.58579",
|
||||||
"x": "1.00000",
|
"x": "0.70711",
|
||||||
"y": "3.98333",
|
"y": "3.82861",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -1775,7 +1767,7 @@ History {
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "22.36242",
|
"height": "22.07000",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
@ -1788,8 +1780,8 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"99.27949",
|
||||||
"-22.36242",
|
"-22.07000",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -1810,9 +1802,9 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": "98.00000",
|
"width": "99.27949",
|
||||||
"x": 1,
|
"x": "0.01341",
|
||||||
"y": 34,
|
"y": "33.34227",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
@ -2322,14 +2314,13 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id185",
|
"elementId": "id185",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "408.19672",
|
"height": "410.63965",
|
||||||
"id": "id186",
|
"id": "id186",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2343,8 +2334,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
498,
|
"501.24760",
|
||||||
"-408.19672",
|
"-410.63965",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -2354,7 +2345,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id184",
|
"elementId": "id184",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -2364,8 +2354,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": 498,
|
"width": "501.24760",
|
||||||
"x": 1,
|
"x": "0.70711",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -2484,8 +2474,7 @@ History {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id185",
|
"elementId": "id185",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -2515,7 +2504,6 @@ History {
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id184",
|
"elementId": "id184",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -15122,8 +15110,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id58",
|
"elementId": "id58",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -15143,7 +15130,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"98.58579",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -15154,7 +15141,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id56",
|
"elementId": "id56",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -15164,8 +15150,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": "98.58579",
|
||||||
"x": 1,
|
"x": "0.70711",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -15493,8 +15479,7 @@ History {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id58",
|
"elementId": "id58",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -15524,7 +15509,6 @@ History {
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id56",
|
"elementId": "id56",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -15821,8 +15805,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id52",
|
"elementId": "id52",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -15842,7 +15825,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"98.58579",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -15853,7 +15836,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id50",
|
"elementId": "id50",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -15863,8 +15845,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": "98.58579",
|
||||||
"x": 1,
|
"x": "0.70711",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -16116,8 +16098,7 @@ History {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id52",
|
"elementId": "id52",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -16147,7 +16128,6 @@ History {
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id50",
|
"elementId": "id50",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -16444,8 +16424,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id64",
|
"elementId": "id64",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -16465,7 +16444,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"98.58579",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -16476,7 +16455,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id62",
|
"elementId": "id62",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -16486,8 +16464,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": "98.58579",
|
||||||
"x": 1,
|
"x": "0.70711",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -16739,8 +16717,7 @@ History {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id64",
|
"elementId": "id64",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -16770,7 +16747,6 @@ History {
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id62",
|
"elementId": "id62",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -17065,8 +17041,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id70",
|
"elementId": "id70",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -17086,7 +17061,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"98.58579",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -17097,7 +17072,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id68",
|
"elementId": "id68",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -17107,8 +17081,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": "98.58579",
|
||||||
"x": 1,
|
"x": "0.70711",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -17170,7 +17144,6 @@ History {
|
|||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id68",
|
"elementId": "id68",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -17431,8 +17404,7 @@ History {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id70",
|
"elementId": "id70",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -17462,7 +17434,6 @@ History {
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id68",
|
"elementId": "id68",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -17783,8 +17754,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id77",
|
"elementId": "id77",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -17804,7 +17774,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"98.58579",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -17815,7 +17785,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id75",
|
"elementId": "id75",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -17825,8 +17794,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"width": "98.00000",
|
"width": "98.58579",
|
||||||
"x": 1,
|
"x": "0.70711",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -17887,8 +17856,7 @@ History {
|
|||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id77",
|
"elementId": "id77",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"points": [
|
"points": [
|
||||||
@ -17903,7 +17871,6 @@ History {
|
|||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id75",
|
"elementId": "id75",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@ -18165,8 +18132,7 @@ History {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id77",
|
"elementId": "id77",
|
||||||
"fixedPoint": null,
|
"focus": -0,
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@ -18196,7 +18162,6 @@ History {
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id75",
|
"elementId": "id75",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
|
@ -190,14 +190,13 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id1",
|
"elementId": "id1",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "-0.46667",
|
"focus": "-0.46667",
|
||||||
"gap": 10,
|
"gap": 10,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "81.47368",
|
"height": "84.41974",
|
||||||
"id": "id2",
|
"id": "id2",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -211,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
81,
|
"83.92893",
|
||||||
"81.47368",
|
"84.41974",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -223,7 +222,6 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"fixedPoint": null,
|
|
||||||
"focus": "-0.60000",
|
"focus": "-0.60000",
|
||||||
"gap": 10,
|
"gap": 10,
|
||||||
},
|
},
|
||||||
@ -234,7 +232,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1051383431,
|
"versionNonce": 1051383431,
|
||||||
"width": 81,
|
"width": "83.92893",
|
||||||
"x": 110,
|
"x": 110,
|
||||||
"y": 50,
|
"y": 50,
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,6 @@ describe("element binding", () => {
|
|||||||
|
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
@ -77,13 +76,11 @@ describe("element binding", () => {
|
|||||||
// Both the start and the end points should be bound
|
// Both the start and the end points should be bound
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
togglePopover,
|
togglePopover,
|
||||||
getCloneByOrigId,
|
getCloneByOrigId,
|
||||||
} from "./test-utils";
|
} from "./test-utils";
|
||||||
|
import "../../utils/test-utils";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
@ -1321,13 +1322,11 @@ describe("history", () => {
|
|||||||
expect(API.getUndoStack().length).toBe(5);
|
expect(API.getUndoStack().length).toBe(5);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
@ -1346,13 +1345,11 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
@ -1371,13 +1368,11 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
@ -1404,13 +1399,11 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
@ -1429,13 +1422,11 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
});
|
});
|
||||||
@ -1486,13 +1477,11 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -1533,13 +1522,11 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -1614,13 +1601,11 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -1689,13 +1674,11 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -4276,13 +4259,11 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -4347,13 +4328,11 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -4414,13 +4393,11 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -4489,14 +4466,12 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
// rebound with previous rectangle
|
// rebound with previous rectangle
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
@ -4788,14 +4763,12 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
focus: -0,
|
||||||
focus: 0,
|
|
||||||
gap: 1,
|
gap: 1,
|
||||||
}),
|
}),
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
@ -4838,13 +4811,11 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
fixedPoint: null,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
gap: expect.toBeNonNaNNumber(),
|
||||||
}),
|
}),
|
||||||
|
@ -1238,7 +1238,7 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.downAt(rect.x, rect.y);
|
mouse.downAt(rect.x, rect.y);
|
||||||
mouse.moveTo(200, 0);
|
mouse.moveTo(200, 0);
|
||||||
mouse.upAt(200, 0);
|
mouse.upAt(200, 0);
|
||||||
expect(arrow.width).toBe(200);
|
expect(arrow.width).toBeCloseTo(204, 0);
|
||||||
expect(rect.x).toBe(200);
|
expect(rect.x).toBe(200);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||||
|
@ -123,7 +123,7 @@ describe("move element", () => {
|
|||||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||||
expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([110, 50]);
|
expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([110, 50]);
|
||||||
expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
|
expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
|
||||||
81, 81,
|
84, 84,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
|
@ -181,12 +181,12 @@ describe("generic element", () => {
|
|||||||
|
|
||||||
UI.resize(rectangle, "e", [40, 0]);
|
UI.resize(rectangle, "e", [40, 0]);
|
||||||
|
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30);
|
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||||
|
|
||||||
UI.resize(rectangle, "w", [50, 0]);
|
UI.resize(rectangle, "w", [50, 0]);
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80);
|
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with a label", async () => {
|
it("resizes with a label", async () => {
|
||||||
@ -501,12 +501,12 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize(rectangle, "se", [-200, -150]);
|
UI.resize(rectangle, "se", [-200, -150]);
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -529,13 +529,13 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.13);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.11);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -811,15 +811,16 @@ describe("image element", () => {
|
|||||||
|
|
||||||
UI.resize(image, "ne", [40, 0]);
|
UI.resize(image, "ne", [40, 0]);
|
||||||
|
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30);
|
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0);
|
||||||
|
|
||||||
const imageWidth = image.width;
|
const imageWidth = image.width;
|
||||||
const scale = 20 / image.height;
|
const scale = 20 / image.height;
|
||||||
UI.resize(image, "nw", [50, 20]);
|
UI.resize(image, "nw", [50, 20]);
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toEqual(image.id);
|
expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(
|
expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
|
||||||
30 + imageWidth * scale,
|
30 + imageWidth * scale,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1024,7 +1025,7 @@ describe("multiple selection", () => {
|
|||||||
|
|
||||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||||
expect(leftBoundArrow.width).toBeCloseTo(140, 0);
|
expect(leftBoundArrow.width).toBeCloseTo(143, 0);
|
||||||
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
||||||
expect(leftBoundArrow.angle).toEqual(0);
|
expect(leftBoundArrow.angle).toEqual(0);
|
||||||
expect(leftBoundArrow.startBinding).toBeNull();
|
expect(leftBoundArrow.startBinding).toBeNull();
|
||||||
@ -1046,7 +1047,9 @@ describe("multiple selection", () => {
|
|||||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||||
rightArrowBinding.elementId,
|
rightArrowBinding.elementId,
|
||||||
);
|
);
|
||||||
expect(rightBoundArrow.endBinding?.focus).toBe(rightArrowBinding.focus);
|
expect(rightBoundArrow.endBinding?.focus).toBeCloseTo(
|
||||||
|
rightArrowBinding.focus!,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with labeled arrows", async () => {
|
it("resizes with labeled arrows", async () => {
|
||||||
|
@ -32,7 +32,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
|||||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||||
expect(arrow.x).toBeCloseTo(-80);
|
expect(arrow.x).toBeCloseTo(-80);
|
||||||
expect(arrow.y).toBeCloseTo(50);
|
expect(arrow.y).toBeCloseTo(50);
|
||||||
expect(arrow.width).toBeCloseTo(110.7, 1);
|
expect(arrow.width).toBeCloseTo(116.7, 1);
|
||||||
expect(arrow.height).toBeCloseTo(0);
|
expect(arrow.height).toBeCloseTo(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,8 +69,8 @@ test("unselected bound arrows update when rotating their target elements", async
|
|||||||
expect(ellipseArrow.x).toEqual(0);
|
expect(ellipseArrow.x).toEqual(0);
|
||||||
expect(ellipseArrow.y).toEqual(0);
|
expect(ellipseArrow.y).toEqual(0);
|
||||||
expect(ellipseArrow.points[0]).toEqual([0, 0]);
|
expect(ellipseArrow.points[0]).toEqual([0, 0]);
|
||||||
expect(ellipseArrow.points[1][0]).toBeCloseTo(48.5, 1);
|
expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
|
||||||
expect(ellipseArrow.points[1][1]).toBeCloseTo(126.5, 1);
|
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
|
||||||
|
|
||||||
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
||||||
expect(textArrow.x).toEqual(360);
|
expect(textArrow.x).toEqual(360);
|
||||||
|
@ -16,7 +16,6 @@ import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
|
|||||||
import { getSelectedElements } from "../scene/selection";
|
import { getSelectedElements } from "../scene/selection";
|
||||||
import type { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { UI } from "./helpers/ui";
|
import { UI } from "./helpers/ui";
|
||||||
import { diffStringsUnified } from "jest-diff";
|
|
||||||
import ansi from "ansicolor";
|
import ansi from "ansicolor";
|
||||||
import { ORIG_ID } from "../constants";
|
import { ORIG_ID } from "../constants";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
@ -259,36 +258,6 @@ expect.extend({
|
|||||||
pass: false,
|
pass: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
toCloselyEqualPoints(received, expected, precision) {
|
|
||||||
if (!Array.isArray(received) || !Array.isArray(expected)) {
|
|
||||||
throw new Error("expected and received are not point arrays");
|
|
||||||
}
|
|
||||||
|
|
||||||
const COMPARE = 1 / Math.pow(10, precision || 2);
|
|
||||||
const pass = received.every(
|
|
||||||
(point, idx) =>
|
|
||||||
Math.abs(expected[idx]?.[0] - point[0]) < COMPARE &&
|
|
||||||
Math.abs(expected[idx]?.[1] - point[1]) < COMPARE,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!pass) {
|
|
||||||
return {
|
|
||||||
message: () => ` The provided array of points are not close enough.
|
|
||||||
|
|
||||||
${diffStringsUnified(
|
|
||||||
JSON.stringify(expected, undefined, 2),
|
|
||||||
JSON.stringify(received, undefined, 2),
|
|
||||||
)}`,
|
|
||||||
pass: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: () => `expected ${received} to not be close to ${expected}`,
|
|
||||||
pass: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { Curve } from "../math";
|
||||||
import {
|
import {
|
||||||
isLineSegment,
|
isLineSegment,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
@ -6,7 +7,7 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
import type { LineSegment } from "../utils";
|
import type { LineSegment } from "../utils";
|
||||||
import type { BoundingBox, Bounds } from "./element/bounds";
|
import type { Bounds } from "./element/bounds";
|
||||||
import { isBounds } from "./element/typeChecks";
|
import { isBounds } from "./element/typeChecks";
|
||||||
|
|
||||||
// The global data holder to collect the debug operations
|
// The global data holder to collect the debug operations
|
||||||
@ -16,17 +17,29 @@ declare global {
|
|||||||
data: DebugElement[][];
|
data: DebugElement[][];
|
||||||
currentFrame?: number;
|
currentFrame?: number;
|
||||||
};
|
};
|
||||||
debugDrawPoint: typeof debugDrawPoint;
|
|
||||||
debugDrawLine: typeof debugDrawLine;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DebugElement = {
|
export type DebugElement = {
|
||||||
color: string;
|
color: string;
|
||||||
data: LineSegment<GlobalPoint>;
|
data: LineSegment<GlobalPoint> | Curve<GlobalPoint>;
|
||||||
permanent: boolean;
|
permanent: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const debugDrawCubicBezier = (
|
||||||
|
c: Curve<GlobalPoint>,
|
||||||
|
opts?: {
|
||||||
|
color?: string;
|
||||||
|
permanent?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
addToCurrentFrame({
|
||||||
|
color: opts?.color ?? "purple",
|
||||||
|
permanent: !!opts?.permanent,
|
||||||
|
data: c,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const debugDrawLine = (
|
export const debugDrawLine = (
|
||||||
segment: LineSegment<GlobalPoint> | LineSegment<GlobalPoint>[],
|
segment: LineSegment<GlobalPoint> | LineSegment<GlobalPoint>[],
|
||||||
opts?: {
|
opts?: {
|
||||||
@ -80,41 +93,6 @@ export const debugDrawPoint = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const debugDrawBoundingBox = (
|
|
||||||
box: BoundingBox | BoundingBox[],
|
|
||||||
opts?: {
|
|
||||||
color?: string;
|
|
||||||
permanent?: boolean;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
(Array.isArray(box) ? box : [box]).forEach((bbox) =>
|
|
||||||
debugDrawLine(
|
|
||||||
[
|
|
||||||
lineSegment(
|
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
|
||||||
),
|
|
||||||
lineSegment(
|
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
|
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
|
||||||
),
|
|
||||||
lineSegment(
|
|
||||||
pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
|
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
|
||||||
),
|
|
||||||
lineSegment(
|
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
|
|
||||||
pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
color: opts?.color ?? "cyan",
|
|
||||||
permanent: opts?.permanent,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const debugDrawBounds = (
|
export const debugDrawBounds = (
|
||||||
box: Bounds | Bounds[],
|
box: Bounds | Bounds[],
|
||||||
opts?: {
|
opts?: {
|
||||||
|
@ -26,7 +26,10 @@ export const normalizeRadians = (angle: Radians): Radians => {
|
|||||||
export const cartesian2Polar = <P extends GlobalPoint | LocalPoint>([
|
export const cartesian2Polar = <P extends GlobalPoint | LocalPoint>([
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
]: P): PolarCoords => [Math.hypot(x, y), Math.atan2(y, x)];
|
]: P): PolarCoords => [
|
||||||
|
Math.hypot(x, y),
|
||||||
|
normalizeRadians(Math.atan2(y, x) as 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;
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import { isPointOnSymmetricArc } from "./arc";
|
|
||||||
import { pointFrom } from "./point";
|
|
||||||
|
|
||||||
describe("point on arc", () => {
|
|
||||||
it("should detect point on simple arc", () => {
|
|
||||||
expect(
|
|
||||||
isPointOnSymmetricArc(
|
|
||||||
{
|
|
||||||
radius: 1,
|
|
||||||
startAngle: -Math.PI / 4,
|
|
||||||
endAngle: Math.PI / 4,
|
|
||||||
},
|
|
||||||
pointFrom(0.92291667, 0.385),
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
it("should not detect point outside of a simple arc", () => {
|
|
||||||
expect(
|
|
||||||
isPointOnSymmetricArc(
|
|
||||||
{
|
|
||||||
radius: 1,
|
|
||||||
startAngle: -Math.PI / 4,
|
|
||||||
endAngle: Math.PI / 4,
|
|
||||||
},
|
|
||||||
pointFrom(-0.92291667, 0.385),
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
it("should not detect point with good angle but incorrect radius", () => {
|
|
||||||
expect(
|
|
||||||
isPointOnSymmetricArc(
|
|
||||||
{
|
|
||||||
radius: 1,
|
|
||||||
startAngle: -Math.PI / 4,
|
|
||||||
endAngle: Math.PI / 4,
|
|
||||||
},
|
|
||||||
pointFrom(-0.5, 0.5),
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,20 +0,0 @@
|
|||||||
import { cartesian2Polar } from "./angle";
|
|
||||||
import type { GlobalPoint, LocalPoint, SymmetricArc } from "./types";
|
|
||||||
import { PRECISION } from "./utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export const isPointOnSymmetricArc = <P extends GlobalPoint | LocalPoint>(
|
|
||||||
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc,
|
|
||||||
point: P,
|
|
||||||
): boolean => {
|
|
||||||
const [radius, angle] = cartesian2Polar(point);
|
|
||||||
|
|
||||||
return startAngle < endAngle
|
|
||||||
? Math.abs(radius - arcRadius) < PRECISION &&
|
|
||||||
startAngle <= angle &&
|
|
||||||
endAngle >= angle
|
|
||||||
: startAngle <= angle || endAngle >= angle;
|
|
||||||
};
|
|
101
packages/math/curve.test.ts
Normal file
101
packages/math/curve.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import "../utils/test-utils";
|
||||||
|
import {
|
||||||
|
curve,
|
||||||
|
curveClosestPoint,
|
||||||
|
curveIntersectLineSegment,
|
||||||
|
curvePointDistance,
|
||||||
|
} from "./curve";
|
||||||
|
import { pointFrom } from "./point";
|
||||||
|
import { lineSegment } from "./segment";
|
||||||
|
|
||||||
|
describe("Math curve", () => {
|
||||||
|
describe("line segment intersection", () => {
|
||||||
|
it("point is found when control points are the same", () => {
|
||||||
|
const c = curve(
|
||||||
|
pointFrom(100, 0),
|
||||||
|
pointFrom(100, 100),
|
||||||
|
pointFrom(100, 100),
|
||||||
|
pointFrom(0, 100),
|
||||||
|
);
|
||||||
|
const l = lineSegment(pointFrom(0, 0), pointFrom(200, 200));
|
||||||
|
|
||||||
|
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([
|
||||||
|
[87.5, 87.5],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("point is found when control points aren't the same", () => {
|
||||||
|
const c = curve(
|
||||||
|
pointFrom(100, 0),
|
||||||
|
pointFrom(100, 60),
|
||||||
|
pointFrom(60, 100),
|
||||||
|
pointFrom(0, 100),
|
||||||
|
);
|
||||||
|
const l = lineSegment(pointFrom(0, 0), pointFrom(200, 200));
|
||||||
|
|
||||||
|
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([
|
||||||
|
[72.5, 72.5],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("points are found when curve is sliced at 3 points", () => {
|
||||||
|
const c = curve(
|
||||||
|
pointFrom(-50, -50),
|
||||||
|
pointFrom(10, -50),
|
||||||
|
pointFrom(10, 50),
|
||||||
|
pointFrom(50, 50),
|
||||||
|
);
|
||||||
|
const l = lineSegment(pointFrom(0, 112.5), pointFrom(90, 0));
|
||||||
|
|
||||||
|
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([[50, 50]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be detected where the determinant is overly precise", () => {
|
||||||
|
const c = curve(
|
||||||
|
pointFrom(41.028864759926016, 12.226249068355052),
|
||||||
|
pointFrom(41.028864759926016, 33.55958240168839),
|
||||||
|
pointFrom(30.362198093259348, 44.22624906835505),
|
||||||
|
pointFrom(9.028864759926016, 44.22624906835505),
|
||||||
|
);
|
||||||
|
const l = lineSegment(
|
||||||
|
pointFrom(-82.30963544324186, -41.19949363038283),
|
||||||
|
|
||||||
|
pointFrom(188.2149592542487, 134.75505940984908),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([
|
||||||
|
[34.4, 34.71],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("point closest to other", () => {
|
||||||
|
it("point can be found", () => {
|
||||||
|
const c = curve(
|
||||||
|
pointFrom(-50, -50),
|
||||||
|
pointFrom(10, -50),
|
||||||
|
pointFrom(10, 50),
|
||||||
|
pointFrom(50, 50),
|
||||||
|
);
|
||||||
|
const p = pointFrom(0, 0);
|
||||||
|
|
||||||
|
expect([curveClosestPoint(c, p)]).toCloselyEqualPoints([
|
||||||
|
[5.965462100367372, -3.04104878946646],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("point shortest distance", () => {
|
||||||
|
it("can be determined", () => {
|
||||||
|
const c = curve(
|
||||||
|
pointFrom(-50, -50),
|
||||||
|
pointFrom(10, -50),
|
||||||
|
pointFrom(10, 50),
|
||||||
|
pointFrom(50, 50),
|
||||||
|
);
|
||||||
|
const p = pointFrom(0, 0);
|
||||||
|
|
||||||
|
expect(curvePointDistance(c, p)).toBeCloseTo(6.695873043213627);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,7 @@
|
|||||||
import { pointFrom, pointRotateRads } from "./point";
|
import type { Bounds } from "../excalidraw/element/bounds";
|
||||||
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
|
import { isPoint, pointDistance, pointFrom } from "./point";
|
||||||
|
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
||||||
|
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -18,206 +20,263 @@ 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>(
|
function gradient(
|
||||||
curve: Curve<Point>,
|
f: (t: number, s: number) => number,
|
||||||
angle: Radians,
|
t0: number,
|
||||||
origin: Point,
|
s0: number,
|
||||||
) => {
|
delta: number = 1e-6,
|
||||||
return curve.map((p) => pointRotateRads(p, origin, angle));
|
): number[] {
|
||||||
};
|
return [
|
||||||
|
(f(t0 + delta, s0) - f(t0 - delta, s0)) / (2 * delta),
|
||||||
|
(f(t0, s0 + delta) - f(t0, s0 - delta)) / (2 * delta),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function solve(
|
||||||
|
f: (t: number, s: number) => [number, number],
|
||||||
|
t0: number,
|
||||||
|
s0: number,
|
||||||
|
tolerance: number = 1e-3,
|
||||||
|
iterLimit: number = 10,
|
||||||
|
): number[] | null {
|
||||||
|
let error = Infinity;
|
||||||
|
let iter = 0;
|
||||||
|
|
||||||
|
while (error >= tolerance) {
|
||||||
|
if (iter >= iterLimit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const y0 = f(t0, s0);
|
||||||
|
const jacobian = [
|
||||||
|
gradient((t, s) => f(t, s)[0], t0, s0),
|
||||||
|
gradient((t, s) => f(t, s)[1], t0, s0),
|
||||||
|
];
|
||||||
|
const b = [[-y0[0]], [-y0[1]]];
|
||||||
|
const det =
|
||||||
|
jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
|
||||||
|
|
||||||
|
if (det === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iJ = [
|
||||||
|
[jacobian[1][1] / det, -jacobian[0][1] / det],
|
||||||
|
[-jacobian[1][0] / det, jacobian[0][0] / det],
|
||||||
|
];
|
||||||
|
const h = [
|
||||||
|
[iJ[0][0] * b[0][0] + iJ[0][1] * b[1][0]],
|
||||||
|
[iJ[1][0] * b[0][0] + iJ[1][1] * b[1][0]],
|
||||||
|
];
|
||||||
|
|
||||||
|
t0 = t0 + h[0][0];
|
||||||
|
s0 = s0 + h[1][0];
|
||||||
|
|
||||||
|
const [tErr, sErr] = f(t0, s0);
|
||||||
|
error = Math.max(Math.abs(tErr), Math.abs(sErr));
|
||||||
|
iter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [t0, s0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
c: Curve<Point>,
|
||||||
|
t: number,
|
||||||
|
) =>
|
||||||
|
pointFrom<Point>(
|
||||||
|
(1 - t) ** 3 * c[0][0] +
|
||||||
|
3 * (1 - t) ** 2 * t * c[1][0] +
|
||||||
|
3 * (1 - t) * t ** 2 * c[2][0] +
|
||||||
|
t ** 3 * c[3][0],
|
||||||
|
(1 - t) ** 3 * c[0][1] +
|
||||||
|
3 * (1 - t) ** 2 * t * c[1][1] +
|
||||||
|
3 * (1 - t) * t ** 2 * c[2][1] +
|
||||||
|
t ** 3 * c[3][1],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Computes the intersection between a cubic spline and a line segment.
|
||||||
* @param pointsIn
|
|
||||||
* @param curveTightness
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
export function curveIntersectLineSegment<
|
||||||
pointsIn: readonly Point[],
|
Point extends GlobalPoint | LocalPoint,
|
||||||
curveTightness = 0,
|
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
|
||||||
): Point[] {
|
// Optimize by doing a cheap bounding box check first
|
||||||
const len = pointsIn.length;
|
const bounds = curveBounds(c);
|
||||||
if (len < 3) {
|
if (
|
||||||
throw new Error("A curve must have at least three points.");
|
rectangleIntersectLineSegment(
|
||||||
|
rectangle(
|
||||||
|
pointFrom(bounds[0], bounds[1]),
|
||||||
|
pointFrom(bounds[2], bounds[3]),
|
||||||
|
),
|
||||||
|
l,
|
||||||
|
).length === 0
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
const out: Point[] = [];
|
|
||||||
if (len === 3) {
|
const line = (s: number) =>
|
||||||
out.push(
|
pointFrom<Point>(
|
||||||
pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
l[0][0] + s * (l[1][0] - l[0][0]),
|
||||||
pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
l[0][1] + s * (l[1][1] - l[0][1]),
|
||||||
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
|
||||||
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
const points: Point[] = [];
|
const initial_guesses: [number, number][] = [
|
||||||
points.push(pointsIn[0], pointsIn[0]);
|
[0.5, 0],
|
||||||
for (let i = 1; i < pointsIn.length; i++) {
|
[0.2, 0],
|
||||||
points.push(pointsIn[i]);
|
[0.8, 0],
|
||||||
if (i === pointsIn.length - 1) {
|
];
|
||||||
points.push(pointsIn[i]);
|
|
||||||
}
|
const calculate = ([t0, s0]: [number, number]) => {
|
||||||
}
|
const solution = solve(
|
||||||
const b: Point[] = [];
|
(t: number, s: number) => {
|
||||||
const s = 1 - curveTightness;
|
const bezier_point = bezierEquation(c, t);
|
||||||
out.push(pointFrom(points[0][0], points[0][1]));
|
const line_point = line(s);
|
||||||
for (let i = 1; i + 2 < points.length; i++) {
|
|
||||||
const cachedVertArray = points[i];
|
return [
|
||||||
b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]);
|
bezier_point[0] - line_point[0],
|
||||||
b[1] = pointFrom(
|
bezier_point[1] - line_point[1],
|
||||||
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
|
];
|
||||||
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
|
},
|
||||||
|
t0,
|
||||||
|
s0,
|
||||||
);
|
);
|
||||||
b[2] = pointFrom(
|
|
||||||
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
|
if (!solution) {
|
||||||
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
|
return null;
|
||||||
);
|
|
||||||
b[3] = pointFrom(points[i + 1][0], points[i + 1][1]);
|
|
||||||
out.push(b[1], b[2], b[3]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [t, s] = solution;
|
||||||
|
|
||||||
|
if (t < 0 || t > 1 || s < 0 || s > 1) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
|
return bezierEquation(c, t);
|
||||||
|
};
|
||||||
|
|
||||||
|
let solution = calculate(initial_guesses[0]);
|
||||||
|
if (solution) {
|
||||||
|
return [solution];
|
||||||
|
}
|
||||||
|
|
||||||
|
solution = calculate(initial_guesses[1]);
|
||||||
|
if (solution) {
|
||||||
|
return [solution];
|
||||||
|
}
|
||||||
|
|
||||||
|
solution = calculate(initial_guesses[2]);
|
||||||
|
if (solution) {
|
||||||
|
return [solution];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Finds the closest point on the Bezier curve from another point
|
||||||
*
|
*
|
||||||
* @param t
|
* @param x
|
||||||
* @param controlPoints
|
* @param y
|
||||||
|
* @param P0
|
||||||
|
* @param P1
|
||||||
|
* @param P2
|
||||||
|
* @param P3
|
||||||
|
* @param tolerance
|
||||||
|
* @param maxLevel
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
|
export function curveClosestPoint<Point extends GlobalPoint | LocalPoint>(
|
||||||
t: number,
|
c: Curve<Point>,
|
||||||
controlPoints: Curve<Point>,
|
p: Point,
|
||||||
): Point => {
|
tolerance: number = 1e-3,
|
||||||
const [p0, p1, p2, p3] = controlPoints;
|
): Point | null {
|
||||||
|
const localMinimum = (
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
f: (t: number) => number,
|
||||||
|
e: number = tolerance,
|
||||||
|
) => {
|
||||||
|
let m = min;
|
||||||
|
let n = max;
|
||||||
|
let k;
|
||||||
|
|
||||||
const x =
|
while (n - m > e) {
|
||||||
Math.pow(1 - t, 3) * p0[0] +
|
k = (n + m) / 2;
|
||||||
3 * Math.pow(1 - t, 2) * t * p1[0] +
|
if (f(k - e) < f(k + e)) {
|
||||||
3 * (1 - t) * Math.pow(t, 2) * p2[0] +
|
n = k;
|
||||||
Math.pow(t, 3) * p3[0];
|
} else {
|
||||||
|
m = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const y =
|
return k;
|
||||||
Math.pow(1 - t, 3) * p0[1] +
|
};
|
||||||
3 * Math.pow(1 - t, 2) * t * p1[1] +
|
|
||||||
3 * (1 - t) * Math.pow(t, 2) * p2[1] +
|
|
||||||
Math.pow(t, 3) * p3[1];
|
|
||||||
|
|
||||||
return pointFrom(x, y);
|
const maxSteps = 30;
|
||||||
};
|
let closestStep = 0;
|
||||||
|
for (let min = Infinity, step = 0; step < maxSteps; step++) {
|
||||||
|
const d = pointDistance(p, bezierEquation(c, step / maxSteps));
|
||||||
|
if (d < min) {
|
||||||
|
min = d;
|
||||||
|
closestStep = step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const t0 = Math.max((closestStep - 1) / maxSteps, 0);
|
||||||
|
const t1 = Math.min((closestStep + 1) / maxSteps, 1);
|
||||||
|
const solution = localMinimum(t0, t1, (t) =>
|
||||||
|
pointDistance(p, bezierEquation(c, t)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!solution) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bezierEquation(c, solution);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Determines the distance between a point and the closest point on the
|
||||||
|
* Bezier curve.
|
||||||
*
|
*
|
||||||
* @param point
|
* @param c The curve to test
|
||||||
* @param controlPoints
|
* @param p The point to measure from
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export const cubicBezierDistance = <Point extends LocalPoint | GlobalPoint>(
|
export function curvePointDistance<Point extends GlobalPoint | LocalPoint>(
|
||||||
point: Point,
|
c: Curve<Point>,
|
||||||
controlPoints: Curve<Point>,
|
p: Point,
|
||||||
) => {
|
) {
|
||||||
// Calculate the closest point on the Bezier curve to the given point
|
const closest = curveClosestPoint(c, p);
|
||||||
const t = findClosestParameter(point, controlPoints);
|
|
||||||
|
|
||||||
// Calculate the coordinates of the closest point on the curve
|
if (!closest) {
|
||||||
const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the distance between the given point and the closest point on the curve
|
return pointDistance(p, closest);
|
||||||
const distance = Math.sqrt(
|
}
|
||||||
(point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
|
|
||||||
|
/**
|
||||||
|
* Determines if the parameter is a Curve
|
||||||
|
*/
|
||||||
|
export function isCurve<P extends GlobalPoint | LocalPoint>(
|
||||||
|
v: unknown,
|
||||||
|
): v is Curve<P> {
|
||||||
|
return (
|
||||||
|
Array.isArray(v) &&
|
||||||
|
v.length === 4 &&
|
||||||
|
isPoint(v[0]) &&
|
||||||
|
isPoint(v[1]) &&
|
||||||
|
isPoint(v[2]) &&
|
||||||
|
isPoint(v[3])
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return distance;
|
function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
||||||
};
|
c: Curve<Point>,
|
||||||
|
): Bounds {
|
||||||
const solveCubic = (a: number, b: number, c: number, d: number) => {
|
const [P0, P1, P2, P3] = c;
|
||||||
// This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
|
const x = [P0[0], P1[0], P2[0], P3[0]];
|
||||||
const roots: number[] = [];
|
const y = [P0[1], P1[1], P2[1], P3[1]];
|
||||||
|
return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)];
|
||||||
const discriminant =
|
}
|
||||||
18 * a * b * c * d -
|
|
||||||
4 * Math.pow(b, 3) * d +
|
|
||||||
Math.pow(b, 2) * Math.pow(c, 2) -
|
|
||||||
4 * a * Math.pow(c, 3) -
|
|
||||||
27 * Math.pow(a, 2) * Math.pow(d, 2);
|
|
||||||
|
|
||||||
if (discriminant >= 0) {
|
|
||||||
const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
|
|
||||||
const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
|
|
||||||
|
|
||||||
const root1 = (-b - C - D) / (3 * a);
|
|
||||||
const root2 = (-b + (C + D) / 2) / (3 * a);
|
|
||||||
const root3 = (-b + (C + D) / 2) / (3 * a);
|
|
||||||
|
|
||||||
roots.push(root1, root2, root3);
|
|
||||||
} else {
|
|
||||||
const realPart = -b / (3 * a);
|
|
||||||
|
|
||||||
const root1 =
|
|
||||||
2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
|
|
||||||
const root2 =
|
|
||||||
2 *
|
|
||||||
Math.sqrt(-b / (3 * a)) *
|
|
||||||
Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
|
|
||||||
const root3 =
|
|
||||||
2 *
|
|
||||||
Math.sqrt(-b / (3 * a)) *
|
|
||||||
Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
|
|
||||||
|
|
||||||
roots.push(root1, root2, root3);
|
|
||||||
}
|
|
||||||
|
|
||||||
return roots;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findClosestParameter = <Point extends LocalPoint | GlobalPoint>(
|
|
||||||
point: Point,
|
|
||||||
controlPoints: Curve<Point>,
|
|
||||||
) => {
|
|
||||||
// This function finds the parameter t that minimizes the distance between the point
|
|
||||||
// and any point on the cubic Bezier curve.
|
|
||||||
|
|
||||||
const [p0, p1, p2, p3] = controlPoints;
|
|
||||||
|
|
||||||
// Use the direct formula to find the parameter t
|
|
||||||
const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
|
|
||||||
const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
|
|
||||||
const c = 3 * p1[0] - 3 * p0[0];
|
|
||||||
const d = p0[0] - point[0];
|
|
||||||
|
|
||||||
const rootsX = solveCubic(a, b, c, d);
|
|
||||||
|
|
||||||
// Do the same for the y-coordinate
|
|
||||||
const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
|
|
||||||
const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
|
|
||||||
const g = 3 * p1[1] - 3 * p0[1];
|
|
||||||
const h = p0[1] - point[1];
|
|
||||||
|
|
||||||
const rootsY = solveCubic(e, f, g, h);
|
|
||||||
|
|
||||||
// Select the real root that is between 0 and 1 (inclusive)
|
|
||||||
const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
|
|
||||||
const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
|
|
||||||
|
|
||||||
if (validRootsX.length === 0 || validRootsY.length === 0) {
|
|
||||||
// No valid roots found, use the midpoint as a fallback
|
|
||||||
return 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose the parameter t that minimizes the distance
|
|
||||||
let minDistance = Infinity;
|
|
||||||
let closestT = 0;
|
|
||||||
|
|
||||||
for (const rootX of validRootsX) {
|
|
||||||
for (const rootY of validRootsY) {
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
(rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
|
|
||||||
);
|
|
||||||
if (distance < minDistance) {
|
|
||||||
minDistance = distance;
|
|
||||||
closestT = (rootX + rootY) / 2; // Use the average for a smoother result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return closestT;
|
|
||||||
};
|
|
||||||
|
126
packages/math/ellipse.test.ts
Normal file
126
packages/math/ellipse.test.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
ellipse,
|
||||||
|
ellipseSegmentInterceptPoints,
|
||||||
|
ellipseIncludesPoint,
|
||||||
|
ellipseTouchesPoint,
|
||||||
|
ellipseLineIntersectionPoints,
|
||||||
|
} from "./ellipse";
|
||||||
|
import { line } from "./line";
|
||||||
|
import { pointFrom } from "./point";
|
||||||
|
import { lineSegment } from "./segment";
|
||||||
|
import type { Ellipse, GlobalPoint } from "./types";
|
||||||
|
|
||||||
|
describe("point and ellipse", () => {
|
||||||
|
it("point on ellipse", () => {
|
||||||
|
const target: Ellipse<GlobalPoint> = ellipse(pointFrom(1, 2), 2, 1);
|
||||||
|
[
|
||||||
|
pointFrom(1, 3),
|
||||||
|
pointFrom(1, 1),
|
||||||
|
pointFrom(3, 2),
|
||||||
|
pointFrom(-1, 2),
|
||||||
|
].forEach((p) => {
|
||||||
|
expect(ellipseTouchesPoint(p, target)).toBe(true);
|
||||||
|
});
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(-0.4, 2.7), target, 0.1)).toBe(true);
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(-0.4, 2.71), target, 0.01)).toBe(true);
|
||||||
|
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(2.4, 2.7), target, 0.1)).toBe(true);
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(2.4, 2.71), target, 0.01)).toBe(true);
|
||||||
|
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(2, 1.14), target, 0.1)).toBe(true);
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(2, 1.14), target, 0.01)).toBe(true);
|
||||||
|
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(0, 1.14), target, 0.1)).toBe(true);
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(0, 1.14), target, 0.01)).toBe(true);
|
||||||
|
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(0, 2.8), target)).toBe(false);
|
||||||
|
expect(ellipseTouchesPoint(pointFrom(2, 1.2), target)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("point in ellipse", () => {
|
||||||
|
const target: Ellipse<GlobalPoint> = ellipse(pointFrom(0, 0), 2, 1);
|
||||||
|
[
|
||||||
|
pointFrom(0, 1),
|
||||||
|
pointFrom(0, -1),
|
||||||
|
pointFrom(2, 0),
|
||||||
|
pointFrom(-2, 0),
|
||||||
|
].forEach((p) => {
|
||||||
|
expect(ellipseIncludesPoint(p, target)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ellipseIncludesPoint(pointFrom(-1, 0.8), target)).toBe(true);
|
||||||
|
expect(ellipseIncludesPoint(pointFrom(1, -0.8), target)).toBe(true);
|
||||||
|
|
||||||
|
// Point on outline
|
||||||
|
expect(ellipseIncludesPoint(pointFrom(2, 0), target)).toBe(true);
|
||||||
|
|
||||||
|
expect(ellipseIncludesPoint(pointFrom(-1, 1), target)).toBe(false);
|
||||||
|
expect(ellipseIncludesPoint(pointFrom(-1.4, 0.8), target)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("segment and ellipse", () => {
|
||||||
|
it("detects outside segment", () => {
|
||||||
|
const e = ellipse(pointFrom(0, 0), 2, 2);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
lineSegment<GlobalPoint>(pointFrom(-100, 0), pointFrom(-10, 0)),
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
lineSegment<GlobalPoint>(pointFrom(-10, 0), pointFrom(10, 0)),
|
||||||
|
),
|
||||||
|
).toEqual([pointFrom(-2, 0), pointFrom(2, 0)]);
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
lineSegment<GlobalPoint>(pointFrom(-10, -2), pointFrom(10, -2)),
|
||||||
|
),
|
||||||
|
).toEqual([pointFrom(0, -2)]);
|
||||||
|
expect(
|
||||||
|
ellipseSegmentInterceptPoints(
|
||||||
|
e,
|
||||||
|
lineSegment<GlobalPoint>(pointFrom(0, -1), pointFrom(0, 1)),
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("line and ellipse", () => {
|
||||||
|
const e = ellipse(pointFrom(0, 0), 2, 2);
|
||||||
|
|
||||||
|
it("detects outside line", () => {
|
||||||
|
expect(
|
||||||
|
ellipseLineIntersectionPoints(
|
||||||
|
e,
|
||||||
|
line<GlobalPoint>(pointFrom(-10, -10), pointFrom(10, -10)),
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
it("detects line intersecting ellipse", () => {
|
||||||
|
expect(
|
||||||
|
ellipseLineIntersectionPoints(
|
||||||
|
e,
|
||||||
|
line<GlobalPoint>(pointFrom(0, -1), pointFrom(0, 1)),
|
||||||
|
),
|
||||||
|
).toEqual([pointFrom(0, 2), pointFrom(0, -2)]);
|
||||||
|
expect(
|
||||||
|
ellipseLineIntersectionPoints(
|
||||||
|
e,
|
||||||
|
line<GlobalPoint>(pointFrom(-100, 0), pointFrom(-10, 0)),
|
||||||
|
).map(([x, y]) => pointFrom(Math.round(x), Math.round(y))),
|
||||||
|
).toEqual([pointFrom(2, 0), pointFrom(-2, 0)]);
|
||||||
|
});
|
||||||
|
it("detects line touching ellipse", () => {
|
||||||
|
expect(
|
||||||
|
ellipseLineIntersectionPoints(
|
||||||
|
e,
|
||||||
|
line<GlobalPoint>(pointFrom(-2, -2), pointFrom(2, -2)),
|
||||||
|
),
|
||||||
|
).toEqual([pointFrom(0, -2)]);
|
||||||
|
});
|
||||||
|
});
|
230
packages/math/ellipse.ts
Normal file
230
packages/math/ellipse.ts
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import {
|
||||||
|
pointFrom,
|
||||||
|
pointDistance,
|
||||||
|
pointFromVector,
|
||||||
|
pointsEqual,
|
||||||
|
} from "./point";
|
||||||
|
import type {
|
||||||
|
Ellipse,
|
||||||
|
GlobalPoint,
|
||||||
|
Line,
|
||||||
|
LineSegment,
|
||||||
|
LocalPoint,
|
||||||
|
} from "./types";
|
||||||
|
import { PRECISION } from "./utils";
|
||||||
|
import {
|
||||||
|
vector,
|
||||||
|
vectorAdd,
|
||||||
|
vectorDot,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorScale,
|
||||||
|
} 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 GlobalPoint | LocalPoint>(
|
||||||
|
center: Point,
|
||||||
|
halfWidth: number,
|
||||||
|
halfHeight: number,
|
||||||
|
): Ellipse<Point> {
|
||||||
|
return {
|
||||||
|
center,
|
||||||
|
halfWidth,
|
||||||
|
halfHeight,
|
||||||
|
} as Ellipse<Point>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 GlobalPoint | LocalPoint>(
|
||||||
|
p: Point,
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
) => {
|
||||||
|
const { center, halfWidth, halfHeight } = ellipse;
|
||||||
|
const normalizedX = (p[0] - center[0]) / halfWidth;
|
||||||
|
const normalizedY = (p[1] - center[1]) / halfHeight;
|
||||||
|
|
||||||
|
return normalizedX * normalizedX + normalizedY * normalizedY <= 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 GlobalPoint | LocalPoint>(
|
||||||
|
point: Point,
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
threshold = PRECISION,
|
||||||
|
) => {
|
||||||
|
return ellipseDistanceFromPoint(point, ellipse) <= threshold;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the shortest euclidean distance from a point to the
|
||||||
|
* outline of the ellipse
|
||||||
|
*
|
||||||
|
* @param p The point to consider
|
||||||
|
* @param ellipse The ellipse to calculate the distance to
|
||||||
|
* @returns The eucledian distance
|
||||||
|
*/
|
||||||
|
export const ellipseDistanceFromPoint = <
|
||||||
|
Point extends GlobalPoint | LocalPoint,
|
||||||
|
>(
|
||||||
|
p: Point,
|
||||||
|
ellipse: Ellipse<Point>,
|
||||||
|
): number => {
|
||||||
|
const { halfWidth, halfHeight, center } = ellipse;
|
||||||
|
const a = halfWidth;
|
||||||
|
const b = halfHeight;
|
||||||
|
const translatedPoint = vectorAdd(
|
||||||
|
vectorFromPoint(p),
|
||||||
|
vectorScale(vectorFromPoint(center), -1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const px = Math.abs(translatedPoint[0]);
|
||||||
|
const py = Math.abs(translatedPoint[1]);
|
||||||
|
|
||||||
|
let tx = 0.707;
|
||||||
|
let ty = 0.707;
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const x = a * tx;
|
||||||
|
const y = b * ty;
|
||||||
|
|
||||||
|
const ex = ((a * a - b * b) * tx ** 3) / a;
|
||||||
|
const ey = ((b * b - a * a) * ty ** 3) / b;
|
||||||
|
|
||||||
|
const rx = x - ex;
|
||||||
|
const ry = y - ey;
|
||||||
|
|
||||||
|
const qx = px - ex;
|
||||||
|
const qy = py - ey;
|
||||||
|
|
||||||
|
const r = Math.hypot(ry, rx);
|
||||||
|
const q = Math.hypot(qy, qx);
|
||||||
|
|
||||||
|
tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
|
||||||
|
ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
|
||||||
|
const t = Math.hypot(ty, tx);
|
||||||
|
tx /= t;
|
||||||
|
ty /= t;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minX, minY] = [
|
||||||
|
a * tx * Math.sign(translatedPoint[0]),
|
||||||
|
b * ty * Math.sign(translatedPoint[1]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return pointDistance(pointFromVector(translatedPoint), pointFrom(minX, minY));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a maximum of two intercept points for a line going throug an
|
||||||
|
* ellipse.
|
||||||
|
*/
|
||||||
|
export function ellipseSegmentInterceptPoints<
|
||||||
|
Point extends GlobalPoint | LocalPoint,
|
||||||
|
>(e: Readonly<Ellipse<Point>>, s: Readonly<LineSegment<Point>>): Point[] {
|
||||||
|
const rx = e.halfWidth;
|
||||||
|
const ry = e.halfHeight;
|
||||||
|
|
||||||
|
const dir = vectorFromPoint(s[1], s[0]);
|
||||||
|
const diff = vector(s[0][0] - e.center[0], s[0][1] - e.center[1]);
|
||||||
|
const mDir = vector(dir[0] / (rx * rx), dir[1] / (ry * ry));
|
||||||
|
const mDiff = vector(diff[0] / (rx * rx), diff[1] / (ry * ry));
|
||||||
|
|
||||||
|
const a = vectorDot(dir, mDir);
|
||||||
|
const b = vectorDot(dir, mDiff);
|
||||||
|
const c = vectorDot(diff, mDiff) - 1.0;
|
||||||
|
const d = b * b - a * c;
|
||||||
|
|
||||||
|
const intersections: Point[] = [];
|
||||||
|
|
||||||
|
if (d > 0) {
|
||||||
|
const t_a = (-b - Math.sqrt(d)) / a;
|
||||||
|
const t_b = (-b + Math.sqrt(d)) / a;
|
||||||
|
|
||||||
|
if (0 <= t_a && t_a <= 1) {
|
||||||
|
intersections.push(
|
||||||
|
pointFrom(
|
||||||
|
s[0][0] + (s[1][0] - s[0][0]) * t_a,
|
||||||
|
s[0][1] + (s[1][1] - s[0][1]) * t_a,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 <= t_b && t_b <= 1) {
|
||||||
|
intersections.push(
|
||||||
|
pointFrom(
|
||||||
|
s[0][0] + (s[1][0] - s[0][0]) * t_b,
|
||||||
|
s[0][1] + (s[1][1] - s[0][1]) * t_b,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (d === 0) {
|
||||||
|
const t = -b / a;
|
||||||
|
if (0 <= t && t <= 1) {
|
||||||
|
intersections.push(
|
||||||
|
pointFrom(
|
||||||
|
s[0][0] + (s[1][0] - s[0][0]) * t,
|
||||||
|
s[0][1] + (s[1][1] - s[0][1]) * t,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ellipseLineIntersectionPoints<
|
||||||
|
Point extends GlobalPoint | LocalPoint,
|
||||||
|
>(
|
||||||
|
{ center, halfWidth, halfHeight }: Ellipse<Point>,
|
||||||
|
[g, h]: Line<Point>,
|
||||||
|
): Point[] {
|
||||||
|
const [cx, cy] = center;
|
||||||
|
const x1 = g[0] - cx;
|
||||||
|
const y1 = g[1] - cy;
|
||||||
|
const x2 = h[0] - cx;
|
||||||
|
const y2 = h[1] - cy;
|
||||||
|
const a =
|
||||||
|
Math.pow(x2 - x1, 2) / Math.pow(halfWidth, 2) +
|
||||||
|
Math.pow(y2 - y1, 2) / Math.pow(halfHeight, 2);
|
||||||
|
const b =
|
||||||
|
2 *
|
||||||
|
((x1 * (x2 - x1)) / Math.pow(halfWidth, 2) +
|
||||||
|
(y1 * (y2 - y1)) / Math.pow(halfHeight, 2));
|
||||||
|
const c =
|
||||||
|
Math.pow(x1, 2) / Math.pow(halfWidth, 2) +
|
||||||
|
Math.pow(y1, 2) / Math.pow(halfHeight, 2) -
|
||||||
|
1;
|
||||||
|
const t1 = (-b + Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a);
|
||||||
|
const t2 = (-b - Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a);
|
||||||
|
const candidates = [
|
||||||
|
pointFrom<Point>(x1 + t1 * (x2 - x1) + cx, y1 + t1 * (y2 - y1) + cy),
|
||||||
|
pointFrom<Point>(x1 + t2 * (x2 - x1) + cx, y1 + t2 * (y2 - y1) + cy),
|
||||||
|
].filter((p) => !isNaN(p[0]) && !isNaN(p[1]));
|
||||||
|
|
||||||
|
if (candidates.length === 2 && pointsEqual(candidates[0], candidates[1])) {
|
||||||
|
return [candidates[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
@ -1,70 +0,0 @@
|
|||||||
import * as GA from "./ga";
|
|
||||||
import { point, toString, direction, offset } from "./ga";
|
|
||||||
import * as GAPoint from "./gapoints";
|
|
||||||
import * as GALine from "./galines";
|
|
||||||
import * as GATransform from "./gatransforms";
|
|
||||||
|
|
||||||
describe("geometric algebra", () => {
|
|
||||||
describe("points", () => {
|
|
||||||
it("distanceToLine", () => {
|
|
||||||
const point = GA.point(3, 3);
|
|
||||||
const line = GALine.equation(0, 1, -1);
|
|
||||||
expect(GAPoint.distanceToLine(point, line)).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("distanceToLine neg", () => {
|
|
||||||
const point = GA.point(-3, -3);
|
|
||||||
const line = GALine.equation(0, 1, -1);
|
|
||||||
expect(GAPoint.distanceToLine(point, line)).toEqual(-4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("lines", () => {
|
|
||||||
it("through", () => {
|
|
||||||
const a = GA.point(0, 0);
|
|
||||||
const b = GA.point(2, 0);
|
|
||||||
expect(toString(GALine.through(a, b))).toEqual(
|
|
||||||
toString(GALine.equation(0, 2, 0)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("parallel", () => {
|
|
||||||
const point = GA.point(3, 3);
|
|
||||||
const line = GALine.equation(0, 1, -1);
|
|
||||||
const parallel = GALine.parallel(line, 2);
|
|
||||||
expect(GAPoint.distanceToLine(point, parallel)).toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("translation", () => {
|
|
||||||
it("points", () => {
|
|
||||||
const start = point(2, 2);
|
|
||||||
const move = GATransform.translation(direction(0, 1));
|
|
||||||
const end = GATransform.apply(move, start);
|
|
||||||
expect(toString(end)).toEqual(toString(point(2, 3)));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("points 2", () => {
|
|
||||||
const start = point(2, 2);
|
|
||||||
const move = GATransform.translation(offset(3, 4));
|
|
||||||
const end = GATransform.apply(move, start);
|
|
||||||
expect(toString(end)).toEqual(toString(point(5, 6)));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("lines", () => {
|
|
||||||
const original = GALine.through(point(2, 2), point(3, 4));
|
|
||||||
const move = GATransform.translation(offset(3, 4));
|
|
||||||
const parallel = GATransform.apply(move, original);
|
|
||||||
expect(toString(parallel)).toEqual(
|
|
||||||
toString(GALine.through(point(5, 6), point(6, 8))),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("rotation", () => {
|
|
||||||
it("points", () => {
|
|
||||||
const start = point(2, 2);
|
|
||||||
const pivot = point(1, 1);
|
|
||||||
const rotate = GATransform.rotation(pivot, Math.PI / 2);
|
|
||||||
const end = GATransform.apply(rotate, start);
|
|
||||||
expect(toString(end)).toEqual(toString(point(2, 0)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,317 +0,0 @@
|
|||||||
/**
|
|
||||||
* This is a 2D Projective Geometric Algebra implementation.
|
|
||||||
*
|
|
||||||
* For wider context on geometric algebra visit see https://bivector.net.
|
|
||||||
*
|
|
||||||
* For this specific algebra see cheatsheet https://bivector.net/2DPGA.pdf.
|
|
||||||
*
|
|
||||||
* Converted from generator written by enki, with a ton of added on top.
|
|
||||||
*
|
|
||||||
* This library uses 8-vectors to represent points, directions and lines
|
|
||||||
* in 2D space.
|
|
||||||
*
|
|
||||||
* An array `[a, b, c, d, e, f, g, h]` represents a n(8)vector:
|
|
||||||
* a + b*e0 + c*e1 + d*e2 + e*e01 + f*e20 + g*e12 + h*e012
|
|
||||||
*
|
|
||||||
* See GAPoint, GALine, GADirection and GATransform modules for common
|
|
||||||
* operations.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type Point = NVector;
|
|
||||||
export type Direction = NVector;
|
|
||||||
export type Line = NVector;
|
|
||||||
export type Transform = NVector;
|
|
||||||
|
|
||||||
export const point = (x: number, y: number): Point => [0, 0, 0, 0, y, x, 1, 0];
|
|
||||||
|
|
||||||
export const origin = (): Point => [0, 0, 0, 0, 0, 0, 1, 0];
|
|
||||||
|
|
||||||
export const direction = (x: number, y: number): Direction => {
|
|
||||||
const norm = Math.hypot(x, y); // same as `inorm(direction(x, y))`
|
|
||||||
return [0, 0, 0, 0, y / norm, x / norm, 0, 0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const offset = (x: number, y: number): Direction => [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
y,
|
|
||||||
x,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// This is the "implementation" part of the library
|
|
||||||
|
|
||||||
type NVector = readonly [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
];
|
|
||||||
|
|
||||||
// These are labels for what each number in an nvector represents
|
|
||||||
const NVECTOR_BASE = ["1", "e0", "e1", "e2", "e01", "e20", "e12", "e012"];
|
|
||||||
|
|
||||||
// Used to represent points, lines and transformations
|
|
||||||
export const nvector = (value: number = 0, index: number = 0): NVector => {
|
|
||||||
const result = [0, 0, 0, 0, 0, 0, 0, 0];
|
|
||||||
if (index < 0 || index > 7) {
|
|
||||||
throw new Error(`Expected \`index\` between 0 and 7, got \`${index}\``);
|
|
||||||
}
|
|
||||||
if (value !== 0) {
|
|
||||||
result[index] = value;
|
|
||||||
}
|
|
||||||
return result as unknown as NVector;
|
|
||||||
};
|
|
||||||
|
|
||||||
const STRING_EPSILON = 0.000001;
|
|
||||||
export const toString = (nvector: NVector): string => {
|
|
||||||
const result = nvector
|
|
||||||
.map((value, index) =>
|
|
||||||
Math.abs(value) > STRING_EPSILON
|
|
||||||
? value.toFixed(7).replace(/(\.|0+)$/, "") +
|
|
||||||
(index > 0 ? NVECTOR_BASE[index] : "")
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
.filter((representation) => representation != null)
|
|
||||||
.join(" + ");
|
|
||||||
return result === "" ? "0" : result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reverse the order of the basis blades.
|
|
||||||
export const reverse = (nvector: NVector): NVector => [
|
|
||||||
nvector[0],
|
|
||||||
nvector[1],
|
|
||||||
nvector[2],
|
|
||||||
nvector[3],
|
|
||||||
-nvector[4],
|
|
||||||
-nvector[5],
|
|
||||||
-nvector[6],
|
|
||||||
-nvector[7],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Poincare duality operator.
|
|
||||||
export const dual = (nvector: NVector): NVector => [
|
|
||||||
nvector[7],
|
|
||||||
nvector[6],
|
|
||||||
nvector[5],
|
|
||||||
nvector[4],
|
|
||||||
nvector[3],
|
|
||||||
nvector[2],
|
|
||||||
nvector[1],
|
|
||||||
nvector[0],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Clifford Conjugation
|
|
||||||
export const conjugate = (nvector: NVector): NVector => [
|
|
||||||
nvector[0],
|
|
||||||
-nvector[1],
|
|
||||||
-nvector[2],
|
|
||||||
-nvector[3],
|
|
||||||
-nvector[4],
|
|
||||||
-nvector[5],
|
|
||||||
-nvector[6],
|
|
||||||
nvector[7],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Main involution
|
|
||||||
export const involute = (nvector: NVector): NVector => [
|
|
||||||
nvector[0],
|
|
||||||
-nvector[1],
|
|
||||||
-nvector[2],
|
|
||||||
-nvector[3],
|
|
||||||
nvector[4],
|
|
||||||
nvector[5],
|
|
||||||
nvector[6],
|
|
||||||
-nvector[7],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Multivector addition
|
|
||||||
export const add = (a: NVector, b: NVector | number): NVector => {
|
|
||||||
if (isNumber(b)) {
|
|
||||||
return [a[0] + b, a[1], a[2], a[3], a[4], a[5], a[6], a[7]];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
a[0] + b[0],
|
|
||||||
a[1] + b[1],
|
|
||||||
a[2] + b[2],
|
|
||||||
a[3] + b[3],
|
|
||||||
a[4] + b[4],
|
|
||||||
a[5] + b[5],
|
|
||||||
a[6] + b[6],
|
|
||||||
a[7] + b[7],
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Multivector subtraction
|
|
||||||
export const sub = (a: NVector, b: NVector | number): NVector => {
|
|
||||||
if (isNumber(b)) {
|
|
||||||
return [a[0] - b, a[1], a[2], a[3], a[4], a[5], a[6], a[7]];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
a[0] - b[0],
|
|
||||||
a[1] - b[1],
|
|
||||||
a[2] - b[2],
|
|
||||||
a[3] - b[3],
|
|
||||||
a[4] - b[4],
|
|
||||||
a[5] - b[5],
|
|
||||||
a[6] - b[6],
|
|
||||||
a[7] - b[7],
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
// The geometric product.
|
|
||||||
export const mul = (a: NVector, b: NVector | number): NVector => {
|
|
||||||
if (isNumber(b)) {
|
|
||||||
return [
|
|
||||||
a[0] * b,
|
|
||||||
a[1] * b,
|
|
||||||
a[2] * b,
|
|
||||||
a[3] * b,
|
|
||||||
a[4] * b,
|
|
||||||
a[5] * b,
|
|
||||||
a[6] * b,
|
|
||||||
a[7] * b,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
mulScalar(a, b),
|
|
||||||
b[1] * a[0] +
|
|
||||||
b[0] * a[1] -
|
|
||||||
b[4] * a[2] +
|
|
||||||
b[5] * a[3] +
|
|
||||||
b[2] * a[4] -
|
|
||||||
b[3] * a[5] -
|
|
||||||
b[7] * a[6] -
|
|
||||||
b[6] * a[7],
|
|
||||||
b[2] * a[0] + b[0] * a[2] - b[6] * a[3] + b[3] * a[6],
|
|
||||||
b[3] * a[0] + b[6] * a[2] + b[0] * a[3] - b[2] * a[6],
|
|
||||||
b[4] * a[0] +
|
|
||||||
b[2] * a[1] -
|
|
||||||
b[1] * a[2] +
|
|
||||||
b[7] * a[3] +
|
|
||||||
b[0] * a[4] +
|
|
||||||
b[6] * a[5] -
|
|
||||||
b[5] * a[6] +
|
|
||||||
b[3] * a[7],
|
|
||||||
b[5] * a[0] -
|
|
||||||
b[3] * a[1] +
|
|
||||||
b[7] * a[2] +
|
|
||||||
b[1] * a[3] -
|
|
||||||
b[6] * a[4] +
|
|
||||||
b[0] * a[5] +
|
|
||||||
b[4] * a[6] +
|
|
||||||
b[2] * a[7],
|
|
||||||
b[6] * a[0] + b[3] * a[2] - b[2] * a[3] + b[0] * a[6],
|
|
||||||
b[7] * a[0] +
|
|
||||||
b[6] * a[1] +
|
|
||||||
b[5] * a[2] +
|
|
||||||
b[4] * a[3] +
|
|
||||||
b[3] * a[4] +
|
|
||||||
b[2] * a[5] +
|
|
||||||
b[1] * a[6] +
|
|
||||||
b[0] * a[7],
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mulScalar = (a: NVector, b: NVector): number =>
|
|
||||||
b[0] * a[0] + b[2] * a[2] + b[3] * a[3] - b[6] * a[6];
|
|
||||||
|
|
||||||
// The outer/exterior/wedge product.
|
|
||||||
export const meet = (a: NVector, b: NVector): NVector => [
|
|
||||||
b[0] * a[0],
|
|
||||||
b[1] * a[0] + b[0] * a[1],
|
|
||||||
b[2] * a[0] + b[0] * a[2],
|
|
||||||
b[3] * a[0] + b[0] * a[3],
|
|
||||||
b[4] * a[0] + b[2] * a[1] - b[1] * a[2] + b[0] * a[4],
|
|
||||||
b[5] * a[0] - b[3] * a[1] + b[1] * a[3] + b[0] * a[5],
|
|
||||||
b[6] * a[0] + b[3] * a[2] - b[2] * a[3] + b[0] * a[6],
|
|
||||||
b[7] * a[0] +
|
|
||||||
b[6] * a[1] +
|
|
||||||
b[5] * a[2] +
|
|
||||||
b[4] * a[3] +
|
|
||||||
b[3] * a[4] +
|
|
||||||
b[2] * a[5] +
|
|
||||||
b[1] * a[6],
|
|
||||||
];
|
|
||||||
|
|
||||||
// The regressive product.
|
|
||||||
export const join = (a: NVector, b: NVector): NVector => [
|
|
||||||
joinScalar(a, b),
|
|
||||||
a[1] * b[7] + a[4] * b[5] - a[5] * b[4] + a[7] * b[1],
|
|
||||||
a[2] * b[7] - a[4] * b[6] + a[6] * b[4] + a[7] * b[2],
|
|
||||||
a[3] * b[7] + a[5] * b[6] - a[6] * b[5] + a[7] * b[3],
|
|
||||||
a[4] * b[7] + a[7] * b[4],
|
|
||||||
a[5] * b[7] + a[7] * b[5],
|
|
||||||
a[6] * b[7] + a[7] * b[6],
|
|
||||||
a[7] * b[7],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const joinScalar = (a: NVector, b: NVector): number =>
|
|
||||||
a[0] * b[7] +
|
|
||||||
a[1] * b[6] +
|
|
||||||
a[2] * b[5] +
|
|
||||||
a[3] * b[4] +
|
|
||||||
a[4] * b[3] +
|
|
||||||
a[5] * b[2] +
|
|
||||||
a[6] * b[1] +
|
|
||||||
a[7] * b[0];
|
|
||||||
|
|
||||||
// The inner product.
|
|
||||||
export const dot = (a: NVector, b: NVector): NVector => [
|
|
||||||
b[0] * a[0] + b[2] * a[2] + b[3] * a[3] - b[6] * a[6],
|
|
||||||
b[1] * a[0] +
|
|
||||||
b[0] * a[1] -
|
|
||||||
b[4] * a[2] +
|
|
||||||
b[5] * a[3] +
|
|
||||||
b[2] * a[4] -
|
|
||||||
b[3] * a[5] -
|
|
||||||
b[7] * a[6] -
|
|
||||||
b[6] * a[7],
|
|
||||||
b[2] * a[0] + b[0] * a[2] - b[6] * a[3] + b[3] * a[6],
|
|
||||||
b[3] * a[0] + b[6] * a[2] + b[0] * a[3] - b[2] * a[6],
|
|
||||||
b[4] * a[0] + b[7] * a[3] + b[0] * a[4] + b[3] * a[7],
|
|
||||||
b[5] * a[0] + b[7] * a[2] + b[0] * a[5] + b[2] * a[7],
|
|
||||||
b[6] * a[0] + b[0] * a[6],
|
|
||||||
b[7] * a[0] + b[0] * a[7],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const norm = (a: NVector): number =>
|
|
||||||
Math.sqrt(Math.abs(a[0] * a[0] - a[2] * a[2] - a[3] * a[3] + a[6] * a[6]));
|
|
||||||
|
|
||||||
export const inorm = (a: NVector): number =>
|
|
||||||
Math.sqrt(Math.abs(a[7] * a[7] - a[5] * a[5] - a[4] * a[4] + a[1] * a[1]));
|
|
||||||
|
|
||||||
export const normalized = (a: NVector): NVector => {
|
|
||||||
const n = norm(a);
|
|
||||||
if (n === 0 || n === 1) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
const sign = a[6] < 0 ? -1 : 1;
|
|
||||||
return mul(a, sign / n);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const inormalized = (a: NVector): NVector => {
|
|
||||||
const n = inorm(a);
|
|
||||||
if (n === 0 || n === 1) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return mul(a, 1 / n);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isNumber = (a: any): a is number => typeof a === "number";
|
|
||||||
|
|
||||||
export const E0: NVector = nvector(1, 1);
|
|
||||||
export const E1: NVector = nvector(1, 2);
|
|
||||||
export const E2: NVector = nvector(1, 3);
|
|
||||||
export const E01: NVector = nvector(1, 4);
|
|
||||||
export const E20: NVector = nvector(1, 5);
|
|
||||||
export const E12: NVector = nvector(1, 6);
|
|
||||||
export const E012: NVector = nvector(1, 7);
|
|
||||||
export const I = E012;
|
|
@ -1,26 +0,0 @@
|
|||||||
import * as GA from "./ga";
|
|
||||||
import type { Line, Direction, Point } from "./ga";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A direction is stored as an array `[0, 0, 0, 0, y, x, 0, 0]` representing
|
|
||||||
* vector `(x, y)`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const from = (point: Point): Point => [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
point[4],
|
|
||||||
point[5],
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const fromTo = (from: Point, to: Point): Direction =>
|
|
||||||
GA.inormalized([0, 0, 0, 0, to[4] - from[4], to[5] - from[5], 0, 0]);
|
|
||||||
|
|
||||||
export const orthogonal = (direction: Direction): Direction =>
|
|
||||||
GA.inormalized([0, 0, 0, 0, -direction[5], direction[4], 0, 0]);
|
|
||||||
|
|
||||||
export const orthogonalToLine = (line: Line): Direction => GA.mul(line, GA.I);
|
|
@ -1,52 +0,0 @@
|
|||||||
import * as GA from "./ga";
|
|
||||||
import type { Line, Point } from "./ga";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A line is stored as an array `[0, c, a, b, 0, 0, 0, 0]` representing:
|
|
||||||
* c * e0 + a * e1 + b*e2
|
|
||||||
*
|
|
||||||
* This maps to a standard formula `a * x + b * y + c`.
|
|
||||||
*
|
|
||||||
* `(-b, a)` corresponds to a 2D vector parallel to the line. The lines
|
|
||||||
* have a natural orientation, corresponding to that vector.
|
|
||||||
*
|
|
||||||
* The magnitude ("norm") of the line is `sqrt(a ^ 2 + b ^ 2)`.
|
|
||||||
* `c / norm(line)` is the oriented distance from line to origin.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Returns line with direction (x, y) through origin
|
|
||||||
export const vector = (x: number, y: number): Line =>
|
|
||||||
GA.normalized([0, 0, -y, x, 0, 0, 0, 0]);
|
|
||||||
|
|
||||||
// For equation ax + by + c = 0.
|
|
||||||
export const equation = (a: number, b: number, c: number): Line =>
|
|
||||||
GA.normalized([0, c, a, b, 0, 0, 0, 0]);
|
|
||||||
|
|
||||||
export const through = (from: Point, to: Point): Line =>
|
|
||||||
GA.normalized(GA.join(to, from));
|
|
||||||
|
|
||||||
export const orthogonal = (line: Line, point: Point): Line =>
|
|
||||||
GA.dot(line, point);
|
|
||||||
|
|
||||||
// Returns a line perpendicular to the line through `against` and `intersection`
|
|
||||||
// going through `intersection`.
|
|
||||||
export const orthogonalThrough = (against: Point, intersection: Point): Line =>
|
|
||||||
orthogonal(through(against, intersection), intersection);
|
|
||||||
|
|
||||||
export const parallel = (line: Line, distance: number): Line => {
|
|
||||||
const result = line.slice();
|
|
||||||
result[1] -= distance;
|
|
||||||
return result as unknown as Line;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parallelThrough = (line: Line, point: Point): Line =>
|
|
||||||
orthogonal(orthogonal(point, line), point);
|
|
||||||
|
|
||||||
export const distance = (line1: Line, line2: Line): number =>
|
|
||||||
GA.inorm(GA.meet(line1, line2));
|
|
||||||
|
|
||||||
export const angle = (line1: Line, line2: Line): number =>
|
|
||||||
Math.acos(GA.dot(line1, line2)[0]);
|
|
||||||
|
|
||||||
// The orientation of the line
|
|
||||||
export const sign = (line: Line): number => Math.sign(line[1]);
|
|
@ -1,42 +0,0 @@
|
|||||||
import * as GA from "./ga";
|
|
||||||
import * as GALine from "./galines";
|
|
||||||
import type { Point, Line } from "./ga";
|
|
||||||
import { join } from "./ga";
|
|
||||||
|
|
||||||
export const from = ([x, y]: readonly [number, number]): Point => [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
y,
|
|
||||||
x,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const toTuple = (point: Point): [number, number] => [point[5], point[4]];
|
|
||||||
|
|
||||||
export const abs = (point: Point): Point => [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
Math.abs(point[4]),
|
|
||||||
Math.abs(point[5]),
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const intersect = (line1: Line, line2: Line): Point =>
|
|
||||||
GA.normalized(GA.meet(line1, line2));
|
|
||||||
|
|
||||||
// Projects `point` onto the `line`.
|
|
||||||
// The returned point is the closest point on the `line` to the `point`.
|
|
||||||
export const project = (point: Point, line: Line): Point =>
|
|
||||||
intersect(GALine.orthogonal(line, point), line);
|
|
||||||
|
|
||||||
export const distance = (point1: Point, point2: Point): number =>
|
|
||||||
GA.norm(join(point1, point2));
|
|
||||||
|
|
||||||
export const distanceToLine = (point: Point, line: Line): number =>
|
|
||||||
GA.joinScalar(point, line);
|
|
@ -1,41 +0,0 @@
|
|||||||
import * as GA from "./ga";
|
|
||||||
import type { Line, Direction, Point, Transform } from "./ga";
|
|
||||||
import * as GADirection from "./gadirections";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: docs
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const rotation = (pivot: Point, angle: number): Transform =>
|
|
||||||
GA.add(GA.mul(pivot, Math.sin(angle / 2)), Math.cos(angle / 2));
|
|
||||||
|
|
||||||
export const translation = (direction: Direction): Transform => [
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
-(0.5 * direction[5]),
|
|
||||||
0.5 * direction[4],
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const translationOrthogonal = (
|
|
||||||
direction: Direction,
|
|
||||||
distance: number,
|
|
||||||
): Transform => {
|
|
||||||
const scale = 0.5 * distance;
|
|
||||||
return [1, 0, 0, 0, scale * direction[4], scale * direction[5], 0, 0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const translationAlong = (line: Line, distance: number): Transform =>
|
|
||||||
GA.add(GA.mul(GADirection.orthogonalToLine(line), 0.5 * distance), 1);
|
|
||||||
|
|
||||||
export const compose = (motor1: Transform, motor2: Transform): Transform =>
|
|
||||||
GA.mul(motor2, motor1);
|
|
||||||
|
|
||||||
export const apply = (
|
|
||||||
motor: Transform,
|
|
||||||
nvector: Point | Direction | Line,
|
|
||||||
): Point | Direction | Line =>
|
|
||||||
GA.normalized(GA.mul(GA.mul(motor, nvector), GA.reverse(motor)));
|
|
@ -1,10 +1,10 @@
|
|||||||
export * from "./arc";
|
|
||||||
export * from "./angle";
|
export * from "./angle";
|
||||||
export * from "./curve";
|
export * from "./curve";
|
||||||
export * from "./line";
|
export * from "./line";
|
||||||
export * from "./point";
|
export * from "./point";
|
||||||
export * from "./polygon";
|
export * from "./polygon";
|
||||||
export * from "./range";
|
export * from "./range";
|
||||||
|
export * from "./rectangle";
|
||||||
export * from "./segment";
|
export * from "./segment";
|
||||||
export * from "./triangle";
|
export * from "./triangle";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
31
packages/math/line.test.ts
Normal file
31
packages/math/line.test.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { line, linesIntersectAt } from "./line";
|
||||||
|
import { pointFrom } from "./point";
|
||||||
|
|
||||||
|
describe("line-line intersections", () => {
|
||||||
|
it("should correctly detect intersection at origin", () => {
|
||||||
|
expect(
|
||||||
|
linesIntersectAt(
|
||||||
|
line(pointFrom(-5, -5), pointFrom(5, 5)),
|
||||||
|
line(pointFrom(5, -5), pointFrom(-5, 5)),
|
||||||
|
),
|
||||||
|
).toEqual(pointFrom(0, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly detect intersection at non-origin", () => {
|
||||||
|
expect(
|
||||||
|
linesIntersectAt(
|
||||||
|
line(pointFrom(0, 0), pointFrom(10, 10)),
|
||||||
|
line(pointFrom(10, 0), pointFrom(0, 10)),
|
||||||
|
),
|
||||||
|
).toEqual(pointFrom(5, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly detect parallel lines", () => {
|
||||||
|
expect(
|
||||||
|
linesIntersectAt(
|
||||||
|
line(pointFrom(0, 0), pointFrom(0, 10)),
|
||||||
|
line(pointFrom(10, 0), pointFrom(10, 10)),
|
||||||
|
),
|
||||||
|
).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
import { pointCenter, pointFrom, pointRotateRads } from "./point";
|
import { pointFrom } from "./point";
|
||||||
import type { GlobalPoint, Line, LocalPoint, Radians } from "./types";
|
import type { GlobalPoint, Line, LocalPoint } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a line from two points.
|
* Create a line from two points.
|
||||||
@ -11,54 +11,6 @@ export function line<P extends GlobalPoint | LocalPoint>(a: P, b: P): Line<P> {
|
|||||||
return [a, b] as Line<P>;
|
return [a, b] as Line<P>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenient point creation from an array of two points.
|
|
||||||
*
|
|
||||||
* @param param0 The array with the two points to convert to a line
|
|
||||||
* @returns The created line
|
|
||||||
*/
|
|
||||||
export function lineFromPointPair<P extends GlobalPoint | LocalPoint>([a, b]: [
|
|
||||||
P,
|
|
||||||
P,
|
|
||||||
]): Line<P> {
|
|
||||||
return line(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*
|
|
||||||
* @param pointArray
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function lineFromPointArray<P extends GlobalPoint | LocalPoint>(
|
|
||||||
pointArray: P[],
|
|
||||||
): Line<P> | undefined {
|
|
||||||
return pointArray.length === 2
|
|
||||||
? line<P>(pointArray[0], pointArray[1])
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the coordinates resulting from rotating the given line about an
|
|
||||||
* origin by an angle in degrees note that when the origin is not given,
|
|
||||||
* the midpoint of the given line is used as the origin
|
|
||||||
*
|
|
||||||
* @param l
|
|
||||||
* @param angle
|
|
||||||
* @param origin
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const lineRotate = <Point extends LocalPoint | GlobalPoint>(
|
|
||||||
l: Line<Point>,
|
|
||||||
angle: Radians,
|
|
||||||
origin?: Point,
|
|
||||||
): Line<Point> => {
|
|
||||||
return line(
|
|
||||||
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
|
|
||||||
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the intersection point (unless the lines are parallel) of two
|
* Determines the intersection point (unless the lines are parallel) of two
|
||||||
* lines
|
* lines
|
||||||
@ -67,10 +19,10 @@ export const lineRotate = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
* @param b
|
* @param b
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const linesIntersectAt = <Point extends GlobalPoint | LocalPoint>(
|
export function linesIntersectAt<Point extends GlobalPoint | LocalPoint>(
|
||||||
a: Line<Point>,
|
a: Line<Point>,
|
||||||
b: Line<Point>,
|
b: Line<Point>,
|
||||||
): Point | null => {
|
): Point | null {
|
||||||
const A1 = a[1][1] - a[0][1];
|
const A1 = a[1][1] - a[0][1];
|
||||||
const B1 = a[0][0] - a[1][0];
|
const B1 = a[0][0] - a[1][0];
|
||||||
const A2 = b[1][1] - b[0][1];
|
const A2 = b[1][1] - b[0][1];
|
||||||
@ -83,4 +35,4 @@ export const linesIntersectAt = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
@ -57,24 +57,9 @@ export function pointFromPair<Point extends GlobalPoint | LocalPoint>(
|
|||||||
*/
|
*/
|
||||||
export function pointFromVector<P extends GlobalPoint | LocalPoint>(
|
export function pointFromVector<P extends GlobalPoint | LocalPoint>(
|
||||||
v: Vector,
|
v: Vector,
|
||||||
|
offset: P = pointFrom(0, 0),
|
||||||
): P {
|
): P {
|
||||||
return v as unknown as P;
|
return pointFrom<P>(offset[0] + v[0], offset[1] + v[1]);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the coordiante object to a point.
|
|
||||||
*
|
|
||||||
* @param coords The coordinate object with x and y properties
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function pointFromCoords<Point extends GlobalPoint | LocalPoint>({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
}: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}) {
|
|
||||||
return [x, y] as Point;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,36 +161,6 @@ export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
|
|||||||
return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add together two points by their coordinates like you'd apply a translation
|
|
||||||
* to a point by a vector.
|
|
||||||
*
|
|
||||||
* @param a One point to act as a basis
|
|
||||||
* @param b The other point to act like the vector to translate by
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function pointAdd<Point extends LocalPoint | GlobalPoint>(
|
|
||||||
a: Point,
|
|
||||||
b: Point,
|
|
||||||
): Point {
|
|
||||||
return pointFrom(a[0] + b[0], a[1] + b[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subtract a point from another point like you'd translate a point by an
|
|
||||||
* invese vector.
|
|
||||||
*
|
|
||||||
* @param a The point to translate
|
|
||||||
* @param b The point which will act like a vector
|
|
||||||
* @returns The resulting point
|
|
||||||
*/
|
|
||||||
export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
|
|
||||||
a: Point,
|
|
||||||
b: Point,
|
|
||||||
): Point {
|
|
||||||
return pointFrom(a[0] - b[0], a[1] - b[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the distance between two points.
|
* Calculate the distance between two points.
|
||||||
*
|
*
|
||||||
|
23
packages/math/rectangle.ts
Normal file
23
packages/math/rectangle.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { pointFrom } from "./point";
|
||||||
|
import { lineSegment, lineSegmentIntersectionPoints } from "./segment";
|
||||||
|
import type { GlobalPoint, LineSegment, LocalPoint, Rectangle } from "./types";
|
||||||
|
|
||||||
|
export function rectangle<P extends GlobalPoint | LocalPoint>(
|
||||||
|
topLeft: P,
|
||||||
|
bottomRight: P,
|
||||||
|
): Rectangle<P> {
|
||||||
|
return [topLeft, bottomRight] as Rectangle<P>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rectangleIntersectLineSegment<
|
||||||
|
Point extends LocalPoint | GlobalPoint,
|
||||||
|
>(r: Rectangle<Point>, l: LineSegment<Point>): Point[] {
|
||||||
|
return [
|
||||||
|
lineSegment(r[0], pointFrom(r[1][0], r[0][1])),
|
||||||
|
lineSegment(pointFrom(r[1][0], r[0][1]), r[1]),
|
||||||
|
lineSegment(r[1], pointFrom(r[0][0], r[1][1])),
|
||||||
|
lineSegment(pointFrom(r[0][0], r[1][1]), r[0]),
|
||||||
|
]
|
||||||
|
.map((s) => lineSegmentIntersectionPoints(l, s))
|
||||||
|
.filter((i): i is Point => !!i);
|
||||||
|
}
|
21
packages/math/segment.test.ts
Normal file
21
packages/math/segment.test.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { pointFrom } from "./point";
|
||||||
|
import { lineSegment, lineSegmentIntersectionPoints } from "./segment";
|
||||||
|
|
||||||
|
describe("line-segment intersections", () => {
|
||||||
|
it("should correctly detect intersection", () => {
|
||||||
|
expect(
|
||||||
|
lineSegmentIntersectionPoints(
|
||||||
|
lineSegment(pointFrom(0, 0), pointFrom(5, 0)),
|
||||||
|
lineSegment(pointFrom(2, -2), pointFrom(3, 2)),
|
||||||
|
),
|
||||||
|
).toEqual(pointFrom(2.5, 0));
|
||||||
|
});
|
||||||
|
it("should correctly detect non-intersection", () => {
|
||||||
|
expect(
|
||||||
|
lineSegmentIntersectionPoints(
|
||||||
|
lineSegment(pointFrom(0, 0), pointFrom(5, 0)),
|
||||||
|
lineSegment(pointFrom(3, 1), pointFrom(4, 4)),
|
||||||
|
),
|
||||||
|
).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
import { line, linesIntersectAt } from "./line";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
pointCenter,
|
pointCenter,
|
||||||
@ -27,14 +28,6 @@ export function lineSegment<P extends GlobalPoint | LocalPoint>(
|
|||||||
return [a, b] as LineSegment<P>;
|
return [a, b] as LineSegment<P>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lineSegmentFromPointArray<P extends GlobalPoint | LocalPoint>(
|
|
||||||
pointArray: P[],
|
|
||||||
): LineSegment<P> | undefined {
|
|
||||||
return pointArray.length === 2
|
|
||||||
? lineSegment<P>(pointArray[0], pointArray[1])
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param segment
|
* @param segment
|
||||||
@ -156,3 +149,26 @@ export const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
|||||||
const dy = y - yy;
|
const dy = y - yy;
|
||||||
return Math.sqrt(dx * dx + dy * dy);
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the intersection point of a segment and a line
|
||||||
|
*
|
||||||
|
* @param l
|
||||||
|
* @param s
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function lineSegmentIntersectionPoints<
|
||||||
|
Point extends GlobalPoint | LocalPoint,
|
||||||
|
>(l: LineSegment<Point>, s: LineSegment<Point>): Point | null {
|
||||||
|
const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
|
||||||
|
|
||||||
|
if (
|
||||||
|
!candidate ||
|
||||||
|
!pointOnLineSegment(candidate, s) ||
|
||||||
|
!pointOnLineSegment(candidate, l)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
@ -85,6 +85,13 @@ export type Triangle<P extends GlobalPoint | LocalPoint> = [
|
|||||||
_brand: "excalimath__triangle";
|
_brand: "excalimath__triangle";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rectangular shape represented by 4 points at its corners
|
||||||
|
*/
|
||||||
|
export type Rectangle<P extends GlobalPoint | LocalPoint> = [a: P, b: P] & {
|
||||||
|
_brand: "excalimath__rectangle";
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Polygon
|
// Polygon
|
||||||
//
|
//
|
||||||
@ -120,11 +127,14 @@ export type PolarCoords = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle
|
An ellipse is specified by its center, angle, and its major and minor axes
|
||||||
* corresponds to (1, 0) cartesian coordinates (point), i.e. to the "right".
|
but for the sake of simplicity, we've used halfWidth and halfHeight instead
|
||||||
|
in replace of semi major and semi minor axes
|
||||||
*/
|
*/
|
||||||
export type SymmetricArc = {
|
export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||||
radius: number;
|
center: Point;
|
||||||
startAngle: number;
|
halfWidth: number;
|
||||||
endAngle: number;
|
halfHeight: number;
|
||||||
|
} & {
|
||||||
|
_brand: "excalimath_ellipse";
|
||||||
};
|
};
|
||||||
|
@ -137,12 +137,9 @@ export function vectorMagnitude(v: Vector) {
|
|||||||
export const vectorNormalize = (v: Vector): Vector => {
|
export const vectorNormalize = (v: Vector): Vector => {
|
||||||
const m = vectorMagnitude(v);
|
const m = vectorMagnitude(v);
|
||||||
|
|
||||||
|
if (m === 0) {
|
||||||
|
return vector(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
return vector(v[0] / m, v[1] / m);
|
return vector(v[0] / m, v[1] / m);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Project the first vector onto the second vector
|
|
||||||
*/
|
|
||||||
export const vectorProjection = (a: Vector, b: Vector) => {
|
|
||||||
return vectorScale(b, vectorDot(a, b) / vectorDot(b, b));
|
|
||||||
};
|
|
||||||
|
33
packages/utils/test-utils.ts
Normal file
33
packages/utils/test-utils.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { diffStringsUnified } from "jest-diff";
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toCloselyEqualPoints(received, expected, precision) {
|
||||||
|
if (!Array.isArray(received) || !Array.isArray(expected)) {
|
||||||
|
throw new Error("expected and received are not point arrays");
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMPARE = 1 / Math.pow(10, precision || 2);
|
||||||
|
const pass = expected.every(
|
||||||
|
(point, idx) =>
|
||||||
|
Math.abs(received[idx]?.[0] - point[0]) < COMPARE &&
|
||||||
|
Math.abs(received[idx]?.[1] - point[1]) < COMPARE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!pass) {
|
||||||
|
return {
|
||||||
|
message: () => ` The provided array of points are not close enough.
|
||||||
|
|
||||||
|
${diffStringsUnified(
|
||||||
|
JSON.stringify(expected, undefined, 2),
|
||||||
|
JSON.stringify(received, undefined, 2),
|
||||||
|
)}`,
|
||||||
|
pass: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: () => `expected ${received} to not be close to ${expected}`,
|
||||||
|
pass: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user