feat: Elbow arrow segment fixing & positioning (#8952)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács 2025-01-17 18:07:03 +01:00 committed by GitHub
parent 8551823da9
commit 91ebf8b0ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 3282 additions and 1716 deletions

View file

@ -10983,7 +10983,9 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"focus": "-0.00161",
"gap": "3.53708",
},
"endIsSpecial": null,
"fillStyle": "solid",
"fixedSegments": null,
"frameId": null,
"groupIds": [],
"height": "448.10100",
@ -11000,9 +11002,13 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
0,
],
[
"451.90000",
"225.95000",
0,
],
[
"225.95000",
"448.10100",
],
[
"451.90000",
"448.10100",
@ -11022,6 +11028,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"focus": "-0.00159",
"gap": 5,
},
"startIsSpecial": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@ -11147,7 +11154,9 @@ History {
"focus": "-0.00161",
"gap": "3.53708",
},
"endIsSpecial": false,
"fillStyle": "solid",
"fixedSegments": [],
"frameId": null,
"groupIds": [],
"height": "236.10000",
@ -11185,6 +11194,7 @@ History {
"focus": "-0.00159",
"gap": 5,
},
"startIsSpecial": false,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,

View file

@ -171,8 +171,8 @@ describe("Crop an image", () => {
// test corner handle aspect ratio preserving
UI.crop(image, "se", naturalWidth, naturalHeight, [initialWidth, 0], true);
expect(image.width / image.height).toBe(resizedWidth / resizedHeight);
expect(image.width).toBeLessThanOrEqual(initialWidth);
expect(image.height).toBeLessThanOrEqual(initialHeight);
expect(image.width).toBeLessThanOrEqual(initialWidth + 0.0001);
expect(image.height).toBeLessThanOrEqual(initialHeight + 0.0001);
// reset
image = API.createElement({ type: "image", width: 200, height: 100 });
@ -194,7 +194,7 @@ describe("Crop an image", () => {
expect(image.width).toBeCloseTo(image.height);
// max height should be reached
expect(image.height).toBeCloseTo(initialHeight);
expect(image.width).toBe(initialHeight);
expect(image.width).toBeCloseTo(initialHeight);
});
});

View file

@ -11,6 +11,7 @@ import type {
ExcalidrawMagicFrameElement,
ExcalidrawElbowArrowElement,
ExcalidrawArrowElement,
FixedSegment,
} from "../../element/types";
import { newElement, newTextElement, newLinearElement } from "../../element";
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
@ -197,6 +198,7 @@ export class API {
? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"]
: never;
elbowed?: boolean;
fixedSegments?: FixedSegment[] | null;
}): T extends "arrow" | "line"
? ExcalidrawLinearElement
: T extends "freedraw"

View file

@ -2084,7 +2084,8 @@ describe("history", () => {
)[0] as ExcalidrawElbowArrowElement;
expect(modifiedArrow.points).toEqual([
[0, 0],
[451.9000000000001, 0],
[225.95000000000005, 0],
[225.95000000000005, 448.10100010002003],
[451.9000000000001, 448.10100010002003],
]);
});

View file

@ -5,7 +5,6 @@ import type {
ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FontString,
SceneElementsMap,
} from "../element/types";
import { Excalidraw, mutateElement } from "../index";
import { reseed } from "../random";
@ -1353,23 +1352,19 @@ describe("Test Linear Elements", () => {
const [origStartX, origStartY] = [line.x, line.y];
act(() => {
LinearElementEditor.movePoints(
line,
[
{
index: 0,
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
},
{
index: line.points.length - 1,
point: pointFrom(
line.points[line.points.length - 1][0] - 10,
line.points[line.points.length - 1][1] - 10,
),
},
],
new Map() as SceneElementsMap,
);
LinearElementEditor.movePoints(line, [
{
index: 0,
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
},
{
index: line.points.length - 1,
point: pointFrom(
line.points[line.points.length - 1][0] - 10,
line.points[line.points.length - 1][1] - 10,
),
},
]);
});
expect(line.x).toBe(origStartX + 10);
expect(line.y).toBe(origStartY + 10);

View file

@ -535,7 +535,7 @@ describe("arrow element", () => {
UI.resize([rectangle, arrow], "nw", [300, 350]);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144, 2);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
});
});

View file

@ -10,6 +10,7 @@ import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
import { getSelectedElements } from "../scene/selection";
import type { ExcalidrawElement } from "../element/types";
import { UI } from "./helpers/ui";
import { diffStringsUnified } from "jest-diff";
const customQueries = {
...queries,
@ -246,6 +247,36 @@ expect.extend({
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,
};
},
});
/**