From 2e1a529c6781b1534d27ca79198dccd40b97ec7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Sat, 25 Apr 2026 12:03:50 +0200 Subject: [PATCH] fix(editor): remove extremely large arrows on restore (#11235) * fix: Temp fix for elbow arrow at restore Co-authored-by: Copilot Signed-off-by: Mark Tolmacs * fix: Speculative fixes to avoid Infinity Co-authored-by: Copilot Signed-off-by: Mark Tolmacs * validate/remove arrow size after point normalization & move binding repairs back * validate even simple arrows * remove x/y check * remove duplicate constant --------- Signed-off-by: Mark Tolmacs Co-authored-by: Copilot Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/binding.ts | 10 +++--- packages/element/src/elbowArrow.ts | 4 +-- packages/excalidraw/data/restore.ts | 47 +++++++++++++++++++++++++---- packages/math/src/constants.ts | 2 -- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 566ef3c4e4..3f80ffbae2 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -1943,9 +1943,9 @@ export const calculateFixedPointForElbowArrowBinding = ( return { fixedPoint: normalizeFixedPoint([ (nonRotatedSnappedGlobalPoint[0] - hoveredElement.x) / - hoveredElement.width, + Math.max(hoveredElement.width, PRECISION), (nonRotatedSnappedGlobalPoint[1] - hoveredElement.y) / - hoveredElement.height, + Math.max(hoveredElement.height, PRECISION), ]), }; }; @@ -1976,9 +1976,11 @@ export const calculateFixedPointForNonElbowArrowBinding = ( // Calculate the ratio relative to the element's bounds const fixedPointX = - (nonRotatedPoint[0] - hoveredElement.x) / hoveredElement.width; + (nonRotatedPoint[0] - hoveredElement.x) / + Math.max(hoveredElement.width, PRECISION); const fixedPointY = - (nonRotatedPoint[1] - hoveredElement.y) / hoveredElement.height; + (nonRotatedPoint[1] - hoveredElement.y) / + Math.max(hoveredElement.height, PRECISION); return { fixedPoint: normalizeFixedPoint([fixedPointX, fixedPointY]), diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 9543b4182f..024b846b12 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -2124,8 +2124,8 @@ const normalizeArrowElementUpdate = ( offsetY < -MAX_POS || offsetY > MAX_POS || offsetX + points[points.length - 1][0] < -MAX_POS || - offsetY + points[points.length - 1][0] > MAX_POS || - offsetX + points[points.length - 1][1] < -MAX_POS || + offsetX + points[points.length - 1][0] > MAX_POS || + offsetY + points[points.length - 1][1] < -MAX_POS || offsetY + points[points.length - 1][1] > MAX_POS ) { console.error( diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 7a8b9e586c..7ffe9b712f 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -96,6 +96,8 @@ type RestoredAppState = Omit< "offsetTop" | "offsetLeft" | "width" | "height" >; +const MAX_ARROW_PX = 75_000; + export const AllowedExcalidrawActiveTools: Record< AppState["activeTool"]["type"], boolean @@ -467,8 +469,8 @@ export const restoreElement = ( element.endArrowhead === undefined ? "arrow" : normalizeArrowhead(element.endArrowhead); - const x: number | undefined = element.x; - const y: number | undefined = element.y; + const x = element.x as number | undefined; + const y = element.y as number | undefined; const points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one !Array.isArray(element.points) || element.points.length < 2 ? [pointFrom(0, 0), pointFrom(element.width, element.height)] @@ -493,8 +495,8 @@ export const restoreElement = ( startArrowhead, endArrowhead, points, - x, - y, + x: x ?? 0, + y: y ?? 0, elbowed: (element as ExcalidrawArrowElement).elbowed, ...getSizeFromPoints(points), }; @@ -513,12 +515,44 @@ export const restoreElement = ( }) : restoreElementWithProperties(element as ExcalidrawArrowElement, base); - return { + const normalizedRestoredElement = { ...restoredElement, ...LinearElementEditor.getNormalizeElementPointsAndCoords( restoredElement, ), }; + + // 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; } // generic elements @@ -666,6 +700,7 @@ export const restoreElements = ( const existingElementsMap = existingElements ? arrayToMap(existingElements) : null; + const restoredElements = syncInvalidIndices( (targetElements || []).reduce((elements, element) => { // filtering out selection, which is legacy, no longer kept in elements, @@ -762,7 +797,7 @@ export const restoreElements = ( } } - // NOTE (mtolmacs): Temporary fix for extremely large arrows + // NOTE (mtolmacs): Temporary fix for invalid/self-bound elbow arrows // Need to iterate again so we have attached text nodes in elementsMap return restoredElements.map((element) => { if ( diff --git a/packages/math/src/constants.ts b/packages/math/src/constants.ts index ce39e42682..6ba5eb8f7f 100644 --- a/packages/math/src/constants.ts +++ b/packages/math/src/constants.ts @@ -1,5 +1,3 @@ -export const PRECISION = 10e-5; - // Legendre-Gauss abscissae (x values) and weights for n=24 // Refeerence: https://pomax.github.io/bezierinfo/legendre-gauss.html export const LegendreGaussN24TValues = [