fix: More arrow fixes

This commit is contained in:
Mark Tolmacs
2025-08-13 20:25:28 +02:00
parent 983a8b49bc
commit 8c751cec45
8 changed files with 173 additions and 21 deletions

View File

@@ -44,3 +44,14 @@ exports[`Test Linear Elements > Test bound text element > should resize and posi
"Online whiteboard "Online whiteboard
collaboration made easy" collaboration made easy"
`; `;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 1`] = `
"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

@@ -1316,7 +1316,7 @@ describe("Test Linear Elements", () => {
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer; const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
expect(arrow.endBinding?.elementId).toBe(rect.id); expect(arrow.endBinding?.elementId).toBe(rect.id);
expect(arrow.width).toBe(400); expect(arrow.width).toBeCloseTo(405);
expect(rect.x).toBe(400); expect(rect.x).toBe(400);
expect(rect.y).toBe(0); expect(rect.y).toBe(0);
expect( expect(
@@ -1335,7 +1335,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).toBeCloseTo(200, 0); expect(arrow.width).toBeCloseTo(205);
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(

View File

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

View File

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

View File

@@ -95,3 +95,142 @@ exports[`move element > rectangle 5`] = `
"y": 40, "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

@@ -79,6 +79,7 @@ describe("move element", () => {
const rectA = UI.createElement("rectangle", { size: 100 }); const rectA = UI.createElement("rectangle", { size: 100 });
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 }); const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
act(() => { act(() => {
// bind line to two rectangles // bind line to two rectangles
bindBindingElement( bindBindingElement(
@@ -92,7 +93,7 @@ describe("move element", () => {
arrow.get() as NonDeleted<ExcalidrawArrowElement>, arrow.get() as NonDeleted<ExcalidrawArrowElement>,
rectB.get(), rectB.get(),
"orbit", "orbit",
"start", "end",
h.app.scene, h.app.scene,
); );
}); });
@@ -109,8 +110,8 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([200, 0]); expect([rectB.x, rectB.y]).toEqual([200, 0]);
expect([arrow.x, arrow.y]).toEqual([110, 50]); expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[105, 45]], 0);
expect([arrow.width, arrow.height]).toEqual([80, 80]); expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[90, 90]], 0);
renderInteractiveScene.mockClear(); renderInteractiveScene.mockClear();
renderStaticScene.mockClear(); renderStaticScene.mockClear();
@@ -128,10 +129,8 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([201, 2]); expect([rectB.x, rectB.y]).toEqual([201, 2]);
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[50, 50]]); expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[105, 45]], 0);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([ expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[91, 91]], 0);
[301.02, 102.02],
]);
h.elements.forEach((element) => expect(element).toMatchSnapshot()); 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.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(81.75, 1);
expect(arrow.height).toBeCloseTo(0); expect(arrow.height).toBeCloseTo(62.3, 1);
}); });
test("unselected bound arrows update when rotating their target elements", async () => { 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.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.98, 1); expect(ellipseArrow.points[1][0]).toBeCloseTo(16.52, 1);
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1); expect(ellipseArrow.points[1][1]).toBeCloseTo(216.57, 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);
expect(textArrow.y).toEqual(300); expect(textArrow.y).toEqual(300);
expect(textArrow.points[0]).toEqual([0, 0]); expect(textArrow.points[0]).toEqual([0, 0]);
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0); expect(textArrow.points[1][0]).toBeCloseTo(-63, 0);
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 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"); 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( const pass = expected.every(
(point, idx) => (point, idx) =>
Math.abs(received[idx]?.[0] - point[0]) < COMPARE && Math.abs(received[idx][0] - point[0]) < COMPARE &&
Math.abs(received[idx]?.[1] - point[1]) < COMPARE, Math.abs(received[idx][1] - point[1]) < COMPARE,
); );
if (!pass) { if (!pass) {