diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index db75c71099..c0b9e0b85f 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -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 = ( + 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(0, 0), pointFrom(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(0, 0), - pointFrom(100, 100), - ], - isDeleted: true, - }; - } - - return normalizedRestoredElement; + return handleOversizedLinearElements(normalizedRestoredElement); } // generic elements diff --git a/packages/excalidraw/tests/data/restore.test.ts b/packages/excalidraw/tests/data/restore.test.ts index df38fc1133..b33f756511 100644 --- a/packages/excalidraw/tests/data/restore.test.ts +++ b/packages/excalidraw/tests/data/restore.test.ts @@ -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",