Compare commits

...

4 Commits

Author SHA1 Message Date
Mark Tolmacs
54159b2309 Do not trigger arrow binding for linear elements 2025-08-13 20:30:38 +02:00
Mark Tolmacs
0983416eff fix: More arrow fixes 2025-08-13 20:25:28 +02:00
Mark Tolmacs
77f728333e fix: Elbow arrow fixes 2025-08-13 17:55:57 +02:00
Mark Tolmacs
e59681e237 Fixing tests 2025-08-13 15:32:57 +02:00
21 changed files with 1521 additions and 1790 deletions

View File

@@ -145,6 +145,7 @@ export const bindOrUnbindBindingElement = (
...opts,
},
);
bindOrUnbindBindingElementEdge(arrow, start, "start", scene);
bindOrUnbindBindingElementEdge(arrow, end, "end", scene);
if (!isElbowArrow(arrow) && (start.focusPoint || end.focusPoint)) {

View File

@@ -45,6 +45,7 @@ import {
import { wrapText } from "./textWrapping";
import {
isArrowElement,
isBindingElement,
isBoundToContainer,
isElbowArrow,
isFrameLikeElement,
@@ -73,7 +74,9 @@ import type {
ExcalidrawImageElement,
ElementsMap,
ExcalidrawElbowArrowElement,
ExcalidrawArrowElement,
} from "./types";
import type { ElementUpdate } from "./mutateElement";
// Returns true when transform (resizing/rotation) happened
export const transformElements = (
@@ -819,13 +822,29 @@ export const resizeSingleElement = (
Number.isFinite(newOrigin.x) &&
Number.isFinite(newOrigin.y)
) {
const updates = {
let updates: ElementUpdate<ExcalidrawElement> = {
...newOrigin,
width: Math.abs(nextWidth),
height: Math.abs(nextHeight),
...rescaledPoints,
};
if (isBindingElement(latestElement)) {
if (latestElement.startBinding) {
updates = {
...updates,
startBinding: null,
} as ElementUpdate<ExcalidrawArrowElement>;
}
if (latestElement.endBinding) {
updates = {
...updates,
endBinding: null,
} as ElementUpdate<ExcalidrawArrowElement>;
}
}
scene.mutateElement(latestElement, updates, {
informMutation: shouldInformMutation,
isDragging: false,

View File

@@ -49,3 +49,9 @@ exports[`Test Linear Elements > Test bound text element > should wrap the bound
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = `
"Online whiteboard
collaboration made
easy"
`;

View File

@@ -33,77 +33,6 @@ describe("element binding", () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
it("should create valid binding if duplicate start/end points", async () => {
const rect = API.createElement({
type: "rectangle",
x: 0,
y: 0,
width: 50,
height: 50,
});
const arrow = API.createElement({
type: "arrow",
x: 100,
y: 0,
width: 100,
height: 1,
points: [
pointFrom(0, 0),
pointFrom(0, 0),
pointFrom(100, 0),
pointFrom(100, 0),
],
});
API.setElements([rect, arrow]);
expect(arrow.startBinding).toBe(null);
// select arrow
mouse.clickAt(150, 0);
// move arrow start to potential binding position
mouse.downAt(100, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
// Point selection is evaluated like the points are rendered,
// from right to left. So clicking on the first point should move the joint,
// not the start point.
expect(arrow.startBinding).toBe(null);
// Now that the start point is free, move it into overlapping position
mouse.downAt(100, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
expect(API.getSelectedElements()).toEqual([arrow]);
expect(arrow.startBinding).toEqual({
elementId: rect.id,
focus: 0,
gap: 0,
fixedPoint: expect.arrayContaining([1.1, 0]),
});
// Move the end point to the overlapping binding position
mouse.downAt(200, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
// Both the start and the end points should be bound
expect(arrow.startBinding).toEqual({
elementId: rect.id,
focus: 0,
gap: 0,
fixedPoint: expect.arrayContaining([1.1, 0]),
});
expect(arrow.endBinding).toEqual({
elementId: rect.id,
focus: 0,
gap: 0,
fixedPoint: expect.arrayContaining([1.1, 0]),
});
});
//@TODO fix the test with rotation
it.skip("rotation of arrow should rebind both ends", () => {
const rectLeft = UI.createElement("rectangle", {
@@ -399,7 +328,7 @@ describe("element binding", () => {
});
// #6459
it("should unbind arrow only from the latest element", () => {
it("should unbind arrow when arrow is resized", () => {
const rectLeft = UI.createElement("rectangle", {
x: 0,
width: 200,
@@ -427,14 +356,13 @@ describe("element binding", () => {
"mouse",
).se!;
Keyboard.keyDown(KEYS.CTRL_OR_CMD);
const elX = handles[0] + handles[2] / 2;
const elY = handles[1] + handles[3] / 2;
mouse.downAt(elX, elY);
mouse.moveTo(300, 400);
mouse.up();
expect(arrow.startBinding).not.toBe(null);
expect(arrow.startBinding).toBe(null);
expect(arrow.endBinding).toBe(null);
});
@@ -538,7 +466,7 @@ describe("Fixed-point arrow binding", () => {
expect(arrow.y).toBe(110);
});
it("should create fixed-point binding when one of the arrow endpoint is inside rectangle", () => {
it("should create orbit binding when one of the arrow endpoint is inside rectangle", () => {
// Create a filled solid rectangle
UI.clickTool("rectangle");
mouse.downAt(100, 100);
@@ -558,8 +486,8 @@ describe("Fixed-point arrow binding", () => {
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
expect(arrow.x).toBe(10);
expect(arrow.y).toBe(10);
expect(arrow.width).toBe(150);
expect(arrow.height).toBe(150);
expect(arrow.width).toBeCloseTo(86.4669660940663);
expect(arrow.height).toBeCloseTo(86.46696609406821);
// Should bind to the rectangle since endpoint is inside
expect(arrow.startBinding).toBe(null);
@@ -581,8 +509,8 @@ describe("Fixed-point arrow binding", () => {
// Check if the arrow moved
expect(arrow.x).toBe(10);
expect(arrow.y).toBe(10);
expect(arrow.width).toBe(300);
expect(arrow.height).toBe(150);
expect(arrow.width).toBeCloseTo(235);
expect(arrow.height).toBeCloseTo(117.5);
});
it("should maintain relative position when arrow start point is dragged outside and rectangle is moved", () => {
@@ -759,7 +687,7 @@ describe("line segment extension binding", () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
it("should use point binding when extended segment intersects element", () => {
it("should bind when extended segment intersects element", () => {
// Create a rectangle that will be intersected by the extended arrow segment
const rect = API.createElement({
type: "rectangle",
@@ -779,14 +707,14 @@ describe("line segment extension binding", () => {
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
// Should create a normal point binding since the extended line segment
// Should create a binding since the extended line segment
// from the last arrow segment intersects the rectangle
expect(arrow.endBinding?.elementId).toBe(rect.id);
expect(arrow.endBinding).toHaveProperty("focus");
expect(arrow.endBinding).toHaveProperty("gap");
expect(arrow.endBinding).toHaveProperty("mode");
expect(arrow.endBinding).toHaveProperty("fixedPoint");
});
it("should use fixed point binding when extended segment misses element", () => {
it("should bind even if the arrow is not pointing at the element", () => {
// Create a rectangle positioned so the extended arrow segment will miss it
const rect = API.createElement({
type: "rectangle",

View File

@@ -155,8 +155,8 @@ describe("elbow arrow routing", () => {
expect(arrow.width).toEqual(90);
expect(arrow.height).toEqual(200);
});
it("can generate proper points for bound elbow arrow", () => {
const scene = new Scene();
const rectangle1 = API.createElement({
type: "rectangle",
x: -150,
@@ -180,17 +180,15 @@ describe("elbow arrow routing", () => {
height: 200,
points: [pointFrom(0, 0), pointFrom(90, 200)],
}) as ExcalidrawElbowArrowElement;
scene.insertElement(rectangle1);
scene.insertElement(rectangle2);
scene.insertElement(arrow);
API.setElements([rectangle1, rectangle2, arrow]);
bindBindingElement(arrow, rectangle1, "orbit", "start", scene);
bindBindingElement(arrow, rectangle2, "orbit", "end", scene);
bindBindingElement(arrow, rectangle1, "orbit", "start", h.scene);
bindBindingElement(arrow, rectangle2, "orbit", "end", h.scene);
expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null);
h.app.scene.mutateElement(arrow, {
h.scene.mutateElement(arrow, {
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
});

View File

@@ -379,7 +379,7 @@ describe("Test Linear Elements", () => {
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`11`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
expect(line.points.length).toEqual(3);
expect(line.points).toMatchInlineSnapshot(`
@@ -549,7 +549,7 @@ describe("Test Linear Elements", () => {
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`14`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
expect(line.points.length).toEqual(5);
@@ -600,7 +600,7 @@ describe("Test Linear Elements", () => {
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`11`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line,
@@ -641,7 +641,7 @@ describe("Test Linear Elements", () => {
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`11`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line,
@@ -689,7 +689,7 @@ describe("Test Linear Elements", () => {
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`17`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`10`);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
@@ -747,7 +747,7 @@ describe("Test Linear Elements", () => {
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`14`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
expect(line.points.length).toEqual(5);
expect((h.elements[0] as ExcalidrawLinearElement).points)
@@ -845,7 +845,7 @@ describe("Test Linear Elements", () => {
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`11`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line,
@@ -1316,7 +1316,7 @@ describe("Test Linear Elements", () => {
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
expect(arrow.endBinding?.elementId).toBe(rect.id);
expect(arrow.width).toBe(400);
expect(arrow.width).toBeCloseTo(405);
expect(rect.x).toBe(400);
expect(rect.y).toBe(0);
expect(
@@ -1335,7 +1335,7 @@ describe("Test Linear Elements", () => {
mouse.downAt(rect.x, rect.y);
mouse.moveTo(200, 0);
mouse.upAt(200, 0);
expect(arrow.width).toBeCloseTo(200, 0);
expect(arrow.width).toBeCloseTo(205);
expect(rect.x).toBe(200);
expect(rect.y).toBe(0);
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(

View File

@@ -1350,8 +1350,8 @@ describe("multiple selection", () => {
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX);
expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY);
expect(boundArrow.points[1][0]).toBeCloseTo(64.1246);
expect(boundArrow.points[1][1]).toBeCloseTo(-85.4995);
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
boundArrow.x + boundArrow.points[1][0] / 2,

View File

@@ -3,6 +3,7 @@ import { pointFrom } from "@excalidraw/math";
import { bindOrUnbindBindingElement } from "@excalidraw/element/binding";
import {
getHoveredElementForBinding,
isSimpleArrow,
isValidPolygon,
LinearElementEditor,
} from "@excalidraw/element";
@@ -82,28 +83,30 @@ export const actionFinalize = register<FormData>({
app.scene,
);
const newArrow = !appState.selectedLinearElement?.selectedPointsIndices;
if (isSimpleArrow(element)) {
const newArrow = !appState.selectedLinearElement?.selectedPointsIndices;
const selectedPointsIndices = newArrow
? [element.points.length - 1] // New arrow creation
: appState.selectedLinearElement.selectedPointsIndices;
const selectedPointsIndices = newArrow
? [element.points.length - 1] // New arrow creation
: appState.selectedLinearElement.selectedPointsIndices;
const draggedPoints: PointsPositionUpdates =
selectedPointsIndices.reduce((map, index) => {
map.set(index, {
point: LinearElementEditor.pointFromAbsoluteCoords(
element,
pointFrom<GlobalPoint>(sceneCoords.x, sceneCoords.y),
elementsMap,
),
});
const draggedPoints: PointsPositionUpdates =
selectedPointsIndices.reduce((map, index) => {
map.set(index, {
point: LinearElementEditor.pointFromAbsoluteCoords(
element,
pointFrom<GlobalPoint>(sceneCoords.x, sceneCoords.y),
elementsMap,
),
});
return map;
}, new Map()) ?? new Map();
return map;
}, new Map()) ?? new Map();
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
newArrow,
});
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
newArrow,
});
}
if (linearElementEditor !== appState.selectedLinearElement) {
// `handlePointerUp()` updated the linear element instance,

View File

@@ -759,6 +759,27 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.registerAction(createRedoAction(this.history));
}
// setState: React.Component<AppProps, AppState>["setState"] = (
// state,
// callback?,
// ) => {
// let newState: Parameters<typeof this.setState>[0] = null;
// if (typeof state === "function") {
// newState = state(this.state, this.props) as Pick<
// AppState,
// keyof AppState
// >;
// } else {
// newState = state as Pick<AppState, keyof AppState>;
// }
// if (newState && Object.hasOwn(newState, "selectedLinearElement")) {
// console.trace(!!newState.selectedLinearElement);
// }
// super.setState(newState, callback);
// };
updateEditorAtom = <Value, Args extends unknown[], Result>(
atom: WritableAtom<Value, Args, Result>,
...args: Args
@@ -2489,47 +2510,6 @@ class App extends React.Component<AppProps, AppState> {
return this.setState(...args);
},
},
watchState: {
configurable: true,
value: (
callback:
| ((
prevState: Parameters<typeof setState>,
nextState: Parameters<typeof setState> | null,
) => void)
| undefined,
) => {
if (callback) {
(window as any).__originalSetState = this.setState;
this.setState = new Proxy(this.setState, {
apply: (target, thisArg, [state, cb]) => {
const prevState = thisArg.state;
let newState: Parameters<typeof setState> | null = null;
// Log state change for debugging
if (typeof state === "function") {
newState = state(prevState, this.props);
} else if (state) {
newState = state;
}
try {
callback(prevState, newState);
} catch (error) {
console.warn("Error in watchState callback:", error);
}
if (newState) {
target.bind(thisArg)(newState as any, cb);
}
},
});
} else if ((window as any).__originalSetState) {
this.setState = (window as any).__originalSetState;
delete (window as any).__originalSetState;
}
},
},
app: {
configurable: true,
value: this,
@@ -7974,7 +7954,13 @@ class App extends React.Component<AppProps, AppState> {
lastCommittedPoint:
multiElement.points[multiElement.points.length - 1],
});
this.actionManager.executeAction(actionFinalize);
this.actionManager.executeAction(actionFinalize, "ui", {
event: event.nativeEvent,
sceneCoords: {
x: pointerDownState.origin.x,
y: pointerDownState.origin.y,
},
});
return;
}
@@ -8161,7 +8147,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState((prevState) => {
let linearElementEditor = null;
let nextSelectedElementIds = prevState.selectedElementIds;
if (isSimpleArrow(element)) {
if (isBindingElement(element)) {
const linearElement = new LinearElementEditor(
element,
this.scene.getNonDeletedElementsMap(),

View File

@@ -4,9 +4,9 @@ import throttle from "lodash.throttle";
import { useEffect, useMemo, useState, memo } from "react";
import { STATS_PANELS } from "@excalidraw/common";
import { getCommonBounds } from "@excalidraw/element";
import { getCommonBounds, isBindingElement } from "@excalidraw/element";
import { getUncroppedWidthAndHeight } from "@excalidraw/element";
import { isElbowArrow, isImageElement } from "@excalidraw/element";
import { isImageElement } from "@excalidraw/element";
import { frameAndChildrenSelectedTogether } from "@excalidraw/element";
@@ -333,7 +333,7 @@ export const StatsInner = memo(
appState={appState}
/>
</StatsRow>
{!isElbowArrow(singleElement) && (
{!isBindingElement(singleElement) && (
<StatsRow>
<Angle
property="angle"

View File

@@ -135,18 +135,7 @@ describe("binding with linear elements", () => {
) as HTMLInputElement;
expect(linear.startBinding).not.toBe(null);
expect(inputX).not.toBeNull();
UI.updateInput(inputX, String("204"));
expect(linear.startBinding).not.toBe(null);
});
it("should remain bound to linear element on small angle change", async () => {
const linear = h.elements[1] as ExcalidrawLinearElement;
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
".drag-input",
) as HTMLInputElement;
expect(linear.startBinding).not.toBe(null);
UI.updateInput(inputAngle, String("1"));
UI.updateInput(inputX, String("186"));
expect(linear.startBinding).not.toBe(null);
});
@@ -161,17 +150,6 @@ describe("binding with linear elements", () => {
UI.updateInput(inputX, String("254"));
expect(linear.startBinding).toBe(null);
});
it("should remain bound to linear element on small angle change", async () => {
const linear = h.elements[1] as ExcalidrawLinearElement;
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
".drag-input",
) as HTMLInputElement;
expect(linear.startBinding).not.toBe(null);
UI.updateInput(inputAngle, String("45"));
expect(linear.startBinding).toBe(null);
});
});
// single element

View File

@@ -439,6 +439,388 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id40",
"type": "text",
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id42",
"fixedPoint": [
0,
0.5001,
],
"mode": "orbit",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"id": Any<String>,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
99,
0,
],
],
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
"elementId": "id41",
"fixedPoint": [
1,
0.5001,
],
"mode": "orbit",
},
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 4,
"versionNonce": Any<Number>,
"width": 100,
"x": 255.5,
"y": 239,
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id39",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
"fontSize": 20,
"frameId": null,
"groupIds": [],
"height": 25,
"id": Any<String>,
"index": "a1",
"isDeleted": false,
"lineHeight": 1.25,
"link": null,
"locked": false,
"opacity": 100,
"originalText": "HELLO WORLD!!",
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"text": "HELLO WORLD!!",
"textAlign": "center",
"type": "text",
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 130,
"x": 240,
"y": 226.5,
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id39",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 100,
"id": Any<String>,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"width": 100,
"x": 155,
"y": 189,
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id39",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 100,
"id": Any<String>,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "ellipse",
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"width": 100,
"x": 355,
"y": 189,
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id44",
"type": "text",
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id46",
"fixedPoint": [
0,
0.5001,
],
"mode": "orbit",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"id": Any<String>,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
99,
0,
],
],
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
"elementId": "id45",
"fixedPoint": [
1,
0.5001,
],
"mode": "orbit",
},
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 4,
"versionNonce": Any<Number>,
"width": 100,
"x": 255.5,
"y": 239,
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id43",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
"fontSize": 20,
"frameId": null,
"groupIds": [],
"height": 25,
"id": Any<String>,
"index": "a1",
"isDeleted": false,
"lineHeight": 1.25,
"link": null,
"locked": false,
"opacity": 100,
"originalText": "HELLO WORLD!!",
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"text": "HELLO WORLD!!",
"textAlign": "center",
"type": "text",
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 130,
"x": 240,
"y": 226.5,
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id43",
"type": "arrow",
},
],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
"fontSize": 20,
"frameId": null,
"groupIds": [],
"height": 25,
"id": Any<String>,
"index": "a2",
"isDeleted": false,
"lineHeight": 1.25,
"link": null,
"locked": false,
"opacity": 100,
"originalText": "HEYYYYY",
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"text": "HEYYYYY",
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"verticalAlign": "top",
"width": 70,
"x": 185,
"y": 226.5,
}
`;
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id43",
"type": "arrow",
},
],
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
"fontSize": 20,
"frameId": null,
"groupIds": [],
"height": 25,
"id": Any<String>,
"index": "a3",
"isDeleted": false,
"lineHeight": 1.25,
"link": null,
"locked": false,
"opacity": 100,
"originalText": "WHATS UP ?",
"roughness": 1,
"roundness": null,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"text": "WHATS UP ?",
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"verticalAlign": "top",
"width": 100,
"x": 355,
"y": 226.5,
}
`;
exports[`Test Transform > should not allow duplicate ids 1`] = `
{
"angle": 0,

View File

@@ -432,12 +432,9 @@ describe("Test Transform", () => {
boundElements: [{ id: text.id, type: "text" }],
startBinding: {
elementId: rectangle.id,
focus: 0,
gap: 0,
},
endBinding: {
elementId: ellipse.id,
focus: 0,
},
});
@@ -517,12 +514,9 @@ describe("Test Transform", () => {
boundElements: [{ id: text1.id, type: "text" }],
startBinding: {
elementId: text2.id,
focus: 0,
gap: 0,
},
endBinding: {
elementId: text3.id,
focus: 0,
},
});
@@ -780,8 +774,8 @@ describe("Test Transform", () => {
const [arrow, rect] = excalidrawElements;
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
elementId: "rect-1",
focus: -0,
gap: 25,
fixedPoint: [-2.05, 0.5001],
mode: "orbit",
});
expect(rect.boundElements).toStrictEqual([
{

View File

@@ -101,7 +101,10 @@ declare module "image-blob-reduce" {
interface CustomMatchers {
toBeNonNaNNumber(): void;
toCloselyEqualPoints(points: readonly [number, number][]): void;
toCloselyEqualPoints(
points: readonly [number, number][],
precision?: number,
): void;
}
declare namespace jest {

File diff suppressed because it is too large Load Diff

View File

@@ -95,3 +95,142 @@ exports[`move element > rectangle 5`] = `
"y": 40,
}
`;
exports[`move element > rectangles with binding arrow 5`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id6",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id0",
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 760410951,
"width": 100,
"x": 0,
"y": 0,
}
`;
exports[`move element > rectangles with binding arrow 6`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id6",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 300,
"id": "id3",
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"seed": 1116226695,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"versionNonce": 271613161,
"width": 300,
"x": 201,
"y": 2,
}
`;
exports[`move element > rectangles with binding arrow 7`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id3",
"fixedPoint": [
"-0.01667",
"0.45000",
],
"mode": "orbit",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "91.98875",
"id": "id6",
"index": "a2",
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"moveMidPointsWithElement": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
91,
"91.98875",
],
],
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 23633383,
"startArrowhead": null,
"startBinding": {
"elementId": "id0",
"fixedPoint": [
"1.05000",
"0.45011",
],
"mode": "orbit",
},
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 14,
"versionNonce": 651223591,
"width": 91,
"x": 105,
"y": "45.01062",
}
`;

View File

@@ -49,8 +49,8 @@ exports[`multi point mode in linear elements > arrow 3`] = `
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 8,
"versionNonce": 1604849351,
"version": 7,
"versionNonce": 400692809,
"width": 70,
"x": 30,
"y": 30,
@@ -104,8 +104,8 @@ exports[`multi point mode in linear elements > line 3`] = `
"strokeWidth": 2,
"type": "line",
"updated": 1,
"version": 8,
"versionNonce": 1604849351,
"version": 7,
"versionNonce": 400692809,
"width": 70,
"x": 30,
"y": 30,

View File

@@ -6161,7 +6161,6 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -6571,7 +6570,10 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"selectedElementIds": {
"id15": true,
},
"selectedLinearElement": null,
"selectedLinearElement": {
"elementId": "id15",
"isEditing": false,
},
},
"inserted": {
"selectedElementIds": {
@@ -6599,13 +6601,10 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"height": 0,
"index": "a5",
"isDeleted": false,
"lastCommittedPoint": [
50,
10,
],
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
@@ -6615,8 +6614,8 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
0,
],
[
50,
10,
0,
0,
],
],
"roughness": 1,
@@ -6629,14 +6628,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"version": 6,
"width": 50,
"version": 3,
"width": 0,
"x": 310,
"y": -10,
},
"inserted": {
"isDeleted": true,
"version": 5,
"version": 2,
},
},
},
@@ -6644,6 +6643,58 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
},
"id": "id17",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id15": {
"deleted": {
"height": 10,
"lastCommittedPoint": [
50,
10,
],
"points": [
[
0,
0,
],
[
50,
10,
],
],
"version": 5,
"width": 50,
},
"inserted": {
"height": 0,
"lastCommittedPoint": null,
"points": [
[
0,
0,
],
[
0,
0,
],
],
"version": 3,
"width": 0,
},
},
},
},
"id": "id19",
},
{
"appState": AppStateDelta {
"delta": Delta {
@@ -6676,7 +6727,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
20,
],
],
"version": 8,
"version": 7,
"width": 80,
},
"inserted": {
@@ -6695,42 +6746,19 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
10,
],
],
"version": 6,
"version": 5,
"width": 50,
},
},
},
},
"id": "id19",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {
"selectedLinearElement": {
"elementId": "id15",
"isEditing": false,
},
},
"inserted": {
"selectedLinearElement": null,
},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {},
},
"id": "id21",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {
"selectedElementIds": {
"id22": true,
},
"selectedElementIds": {},
"selectedLinearElement": null,
},
"inserted": {
@@ -6758,13 +6786,10 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"height": 0,
"index": "a6",
"isDeleted": false,
"lastCommittedPoint": [
50,
10,
],
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
@@ -6774,8 +6799,8 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
0,
],
[
50,
10,
0,
0,
],
],
"polygon": false,
@@ -6787,14 +6812,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "line",
"version": 6,
"width": 50,
"version": 3,
"width": 0,
"x": 430,
"y": -10,
},
"inserted": {
"isDeleted": true,
"version": 5,
"version": 2,
},
},
},
@@ -6802,6 +6827,64 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
},
"id": "id24",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {
"selectedElementIds": {
"id22": true,
},
},
"inserted": {
"selectedElementIds": {},
},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id22": {
"deleted": {
"height": 10,
"lastCommittedPoint": [
50,
10,
],
"points": [
[
0,
0,
],
[
50,
10,
],
],
"version": 5,
"width": 50,
},
"inserted": {
"height": 0,
"lastCommittedPoint": null,
"points": [
[
0,
0,
],
[
0,
0,
],
],
"version": 3,
"width": 0,
},
},
},
},
"id": "id26",
},
{
"appState": AppStateDelta {
"delta": Delta {
@@ -6834,7 +6917,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
20,
],
],
"version": 8,
"version": 7,
"width": 80,
},
"inserted": {
@@ -6853,13 +6936,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
10,
],
],
"version": 6,
"version": 5,
"width": 50,
},
},
},
},
"id": "id26",
"id": "id28",
},
{
"appState": AppStateDelta {
@@ -6880,7 +6963,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"removed": {},
"updated": {},
},
"id": "id28",
"id": "id30",
},
{
"appState": AppStateDelta {
@@ -6900,7 +6983,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"removed": {},
"updated": {},
},
"id": "id30",
"id": "id32",
},
{
"appState": AppStateDelta {
@@ -6919,7 +7002,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"elements": {
"added": {},
"removed": {
"id31": {
"id33": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
@@ -6977,7 +7060,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
},
"updated": {},
},
"id": "id33",
"id": "id35",
},
]
`;
@@ -8634,41 +8717,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": {
"customLineAngle": null,
"elbowed": false,
"elementId": "id0",
"hoverPointIndex": -1,
"isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"arrowOriginalStartPoint": [
10,
10,
],
"lastClickedIsEndPoint": false,
"lastClickedPoint": -1,
"origin": null,
"prevSelectedPointsIndices": null,
"segmentMidpoint": {
"added": false,
"index": null,
"value": null,
},
},
"pointerOffset": {
"x": 0,
"y": 0,
},
"segmentMidPointHoveredCoords": null,
"selectedPointsIndices": null,
"startBindingElement": null,
},
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8734,6 +8786,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"arrowStartIsInside": false,
"lastClickedIsEndPoint": false,
"lastClickedPoint": -1,
"origin": null,
@@ -8896,7 +8949,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8962,6 +9014,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"arrowStartIsInside": false,
"lastClickedIsEndPoint": false,
"lastClickedPoint": -1,
"origin": null,
@@ -9313,41 +9366,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": {
"customLineAngle": null,
"elbowed": false,
"elementId": "id0",
"hoverPointIndex": -1,
"isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"arrowOriginalStartPoint": [
10,
10,
],
"lastClickedIsEndPoint": false,
"lastClickedPoint": -1,
"origin": null,
"prevSelectedPointsIndices": null,
"segmentMidpoint": {
"added": false,
"index": null,
"value": null,
},
},
"pointerOffset": {
"x": 0,
"y": 0,
},
"segmentMidPointHoveredCoords": null,
"selectedPointsIndices": null,
"startBindingElement": null,
},
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -9413,6 +9435,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"arrowStartIsInside": false,
"lastClickedIsEndPoint": false,
"lastClickedPoint": -1,
"origin": null,
@@ -9754,7 +9777,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -9820,6 +9842,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"arrowStartIsInside": false,
"lastClickedIsEndPoint": false,
"lastClickedPoint": -1,
"origin": null,
@@ -14526,37 +14549,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": {
"customLineAngle": null,
"elbowed": false,
"elementId": "id6",
"hoverPointIndex": -1,
"isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"arrowOriginalStartPoint": [
130,
10,
],
"lastClickedIsEndPoint": false,
"lastClickedPoint": -1,
"origin": null,
"prevSelectedPointsIndices": null,
"segmentMidpoint": {
"added": false,
"index": null,
"value": null,
},
},
"pointerOffset": {
"x": 0,
"y": 0,
},
"segmentMidPointHoveredCoords": null,
"selectedPointsIndices": null,
"startBindingElement": null,
},
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -14644,27 +14636,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] number
exports[`regression tests > undo/redo drawing an element > [end of test] redo stack 1`] = `
[
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {
"selectedLinearElement": null,
},
"inserted": {
"selectedLinearElement": {
"elementId": "id6",
"isEditing": false,
},
},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {},
},
"id": "id13",
},
{
"appState": AppStateDelta {
"delta": Delta {
@@ -14693,7 +14664,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
10,
],
],
"version": 9,
"version": 8,
"width": 60,
},
"inserted": {
@@ -14716,12 +14687,64 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
20,
],
],
"version": 8,
"version": 7,
"width": 100,
},
},
},
},
"id": "id13",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id6": {
"deleted": {
"height": 0,
"lastCommittedPoint": null,
"points": [
[
0,
0,
],
[
0,
0,
],
],
"version": 9,
"width": 0,
},
"inserted": {
"height": 10,
"lastCommittedPoint": [
60,
10,
],
"points": [
[
0,
0,
],
[
60,
10,
],
],
"version": 8,
"width": 60,
},
},
},
},
"id": "id14",
},
{
@@ -14731,11 +14754,16 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
"selectedElementIds": {
"id3": true,
},
"selectedLinearElement": null,
},
"inserted": {
"selectedElementIds": {
"id6": true,
},
"selectedLinearElement": {
"elementId": "id6",
"isEditing": false,
},
},
},
},
@@ -14757,13 +14785,10 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"height": 0,
"index": "a2",
"isDeleted": false,
"lastCommittedPoint": [
60,
10,
],
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
@@ -14773,8 +14798,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
0,
],
[
60,
10,
0,
0,
],
],
"roughness": 1,
@@ -14788,7 +14813,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
"strokeWidth": 2,
"type": "arrow",
"version": 9,
"width": 60,
"width": 0,
"x": 130,
"y": 10,
},

View File

@@ -79,6 +79,7 @@ describe("move element", () => {
const rectA = UI.createElement("rectangle", { size: 100 });
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
act(() => {
// bind line to two rectangles
bindBindingElement(
@@ -92,7 +93,7 @@ describe("move element", () => {
arrow.get() as NonDeleted<ExcalidrawArrowElement>,
rectB.get(),
"orbit",
"start",
"end",
h.app.scene,
);
});
@@ -109,8 +110,8 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([200, 0]);
expect([arrow.x, arrow.y]).toEqual([110, 50]);
expect([arrow.width, arrow.height]).toEqual([80, 80]);
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[105, 45]], 0);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[90, 90]], 0);
renderInteractiveScene.mockClear();
renderStaticScene.mockClear();
@@ -128,10 +129,8 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([201, 2]);
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[50, 50]]);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([
[301.02, 102.02],
]);
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[105, 45]], 0);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[91, 91]], 0);
h.elements.forEach((element) => expect(element).toMatchSnapshot());
});

View File

@@ -35,8 +35,8 @@ test("unselected bound arrow updates when rotating its target element", async ()
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
expect(arrow.x).toBeCloseTo(-80);
expect(arrow.y).toBeCloseTo(50);
expect(arrow.width).toBeCloseTo(110.7, 1);
expect(arrow.height).toBeCloseTo(0);
expect(arrow.width).toBeCloseTo(81.75, 1);
expect(arrow.height).toBeCloseTo(62.3, 1);
});
test("unselected bound arrows update when rotating their target elements", async () => {
@@ -72,13 +72,13 @@ test("unselected bound arrows update when rotating their target elements", async
expect(ellipseArrow.x).toEqual(0);
expect(ellipseArrow.y).toEqual(0);
expect(ellipseArrow.points[0]).toEqual([0, 0]);
expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
expect(ellipseArrow.points[1][0]).toBeCloseTo(16.52, 1);
expect(ellipseArrow.points[1][1]).toBeCloseTo(216.57, 1);
expect(textArrow.endBinding?.elementId).toEqual(text.id);
expect(textArrow.x).toEqual(360);
expect(textArrow.y).toEqual(300);
expect(textArrow.points[0]).toEqual([0, 0]);
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
expect(textArrow.points[1][0]).toBeCloseTo(-63, 0);
expect(textArrow.points[1][1]).toBeCloseTo(-146, 0);
});

View File

@@ -6,11 +6,11 @@ expect.extend({
throw new Error("expected and received are not point arrays");
}
const COMPARE = 1 / Math.pow(10, precision || 2);
const COMPARE = 1 / precision === 0 ? 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,
Math.abs(received[idx][0] - point[0]) < COMPARE &&
Math.abs(received[idx][1] - point[1]) < COMPARE,
);
if (!pass) {