fix(editor): prevent freeze from extremely large line elements (#11556)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
minato32
2026-06-25 23:36:02 +05:30
committed by GitHub
parent 2a82821ec5
commit 20f694d110
2 changed files with 83 additions and 33 deletions
+36 -33
View File
@@ -101,7 +101,38 @@ type RestoredAppState = Omit<
"offsetTop" | "offsetLeft" | "width" | "height"
>;
const MAX_ARROW_PX = 75_000;
const MAX_LINEAR_PX = 75_000;
// Last resort fix for extremely large linear elements (lines / arrows), which
// would otherwise freeze the editor while rendering — e.g. a dotted or dashed
// stroke spanning a huge distance generates an enormous dash array.
// https://github.com/excalidraw/excalidraw/issues/11497
const handleOversizedLinearElements = <T extends ExcalidrawLinearElement>(
element: T,
): T => {
if (element.width <= MAX_LINEAR_PX && element.height <= MAX_LINEAR_PX) {
return element;
}
const label =
element.type === "arrow"
? `${isElbowArrow(element) ? "elbow" : "simple"} arrow`
: element.type;
console.error(
`Removing extremely large ${label} ${element.id} (width: ${element.width}, height: ${element.height}, x: ${element.x}, y: ${element.y})`,
);
return {
...element,
x: 0,
y: 0,
width: 100,
height: 100,
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(100, 100)],
isDeleted: true,
};
};
const restoreLinearElementPoints = (
points: unknown,
@@ -560,7 +591,7 @@ export const restoreElement = (
} as ExcalidrawLinearElement));
}
return restoreElementWithProperties(element, {
const restoredLine = restoreElementWithProperties(element, {
type: "line",
startBinding: null,
endBinding: null,
@@ -578,6 +609,8 @@ export const restoreElement = (
: {}),
...getSizeFromPoints(points),
});
return handleOversizedLinearElements(restoredLine);
case "arrow": {
const startArrowhead = normalizeArrowhead(element.startArrowhead);
const endArrowhead =
@@ -644,37 +677,7 @@ export const restoreElement = (
),
};
// Last resort fix for extremely large arrows
if (
normalizedRestoredElement.width > MAX_ARROW_PX ||
normalizedRestoredElement.height > MAX_ARROW_PX
) {
console.error(
`Removing extremely large arrow ${
normalizedRestoredElement.id
} (type: ${
isElbowArrow(normalizedRestoredElement) ? "elbow" : "simple"
}, width: ${normalizedRestoredElement.width}, height: ${
normalizedRestoredElement.height
}, x: ${normalizedRestoredElement.x}, y: ${
normalizedRestoredElement.y
})`,
);
return {
...normalizedRestoredElement,
x: 0,
y: 0,
width: 100,
height: 100,
points: [
pointFrom<LocalPoint>(0, 0),
pointFrom<LocalPoint>(100, 100),
],
isDeleted: true,
};
}
return normalizedRestoredElement;
return handleOversizedLinearElements(normalizedRestoredElement);
}
// generic elements
@@ -526,6 +526,53 @@ describe("restoreElements", () => {
]);
});
it("should mark extremely large linear elements as deleted to avoid freezing", () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => {});
// a degenerate line with astronomical coordinates (see #11497)
const hugeLine: any = API.createElement({
type: "line",
x: 419048829414166,
y: 8484,
});
hugeLine.points = [
[0, 0],
[-302985021938436, 0],
[-838097658820234, 30],
];
const hugeArrow: any = API.createElement({ type: "arrow" });
hugeArrow.points = [
[0, 0],
[900000, 0],
];
const normalLine: any = API.createElement({ type: "line" });
normalLine.points = [
[0, 0],
[100, 200],
];
const [restoredLine, restoredArrow, restoredNormal] =
restore.restoreElements([hugeLine, hugeArrow, normalLine], null);
expect(restoredLine.isDeleted).toBe(true);
expect(restoredLine.width).toBe(100);
expect(restoredLine.height).toBe(100);
expect(restoredArrow.isDeleted).toBe(true);
expect(restoredArrow.width).toBe(100);
expect(restoredArrow.height).toBe(100);
expect(restoredNormal.isDeleted).toBe(false);
expect(restoredNormal.width).toBe(100);
expect(restoredNormal.height).toBe(200);
consoleError.mockRestore();
});
it("when the number of points of a line is greater or equal 2", () => {
const lineElement_0 = API.createElement({
type: "line",