fix(editor): remove extremely large arrows on restore (#11235)

* fix: Temp fix for elbow arrow at restore

Co-authored-by: Copilot <copilot@github.com>
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Speculative fixes to avoid Infinity

Co-authored-by: Copilot <copilot@github.com>
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* 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 <mark@lazycat.hu>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács
2026-04-25 12:03:50 +02:00
committed by GitHub
co-authored by Copilot dwelle
parent b1c6bfcf40
commit 2e1a529c67
4 changed files with 49 additions and 14 deletions
+6 -4
View File
@@ -1943,9 +1943,9 @@ export const calculateFixedPointForElbowArrowBinding = (
return { return {
fixedPoint: normalizeFixedPoint([ fixedPoint: normalizeFixedPoint([
(nonRotatedSnappedGlobalPoint[0] - hoveredElement.x) / (nonRotatedSnappedGlobalPoint[0] - hoveredElement.x) /
hoveredElement.width, Math.max(hoveredElement.width, PRECISION),
(nonRotatedSnappedGlobalPoint[1] - hoveredElement.y) / (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 // Calculate the ratio relative to the element's bounds
const fixedPointX = const fixedPointX =
(nonRotatedPoint[0] - hoveredElement.x) / hoveredElement.width; (nonRotatedPoint[0] - hoveredElement.x) /
Math.max(hoveredElement.width, PRECISION);
const fixedPointY = const fixedPointY =
(nonRotatedPoint[1] - hoveredElement.y) / hoveredElement.height; (nonRotatedPoint[1] - hoveredElement.y) /
Math.max(hoveredElement.height, PRECISION);
return { return {
fixedPoint: normalizeFixedPoint([fixedPointX, fixedPointY]), fixedPoint: normalizeFixedPoint([fixedPointX, fixedPointY]),
+2 -2
View File
@@ -2124,8 +2124,8 @@ const normalizeArrowElementUpdate = (
offsetY < -MAX_POS || offsetY < -MAX_POS ||
offsetY > MAX_POS || offsetY > MAX_POS ||
offsetX + points[points.length - 1][0] < -MAX_POS || offsetX + points[points.length - 1][0] < -MAX_POS ||
offsetY + points[points.length - 1][0] > MAX_POS || offsetX + points[points.length - 1][0] > MAX_POS ||
offsetX + points[points.length - 1][1] < -MAX_POS || offsetY + points[points.length - 1][1] < -MAX_POS ||
offsetY + points[points.length - 1][1] > MAX_POS offsetY + points[points.length - 1][1] > MAX_POS
) { ) {
console.error( console.error(
+41 -6
View File
@@ -96,6 +96,8 @@ type RestoredAppState = Omit<
"offsetTop" | "offsetLeft" | "width" | "height" "offsetTop" | "offsetLeft" | "width" | "height"
>; >;
const MAX_ARROW_PX = 75_000;
export const AllowedExcalidrawActiveTools: Record< export const AllowedExcalidrawActiveTools: Record<
AppState["activeTool"]["type"], AppState["activeTool"]["type"],
boolean boolean
@@ -467,8 +469,8 @@ export const restoreElement = (
element.endArrowhead === undefined element.endArrowhead === undefined
? "arrow" ? "arrow"
: normalizeArrowhead(element.endArrowhead); : normalizeArrowhead(element.endArrowhead);
const x: number | undefined = element.x; const x = element.x as number | undefined;
const y: number | undefined = element.y; const y = element.y as number | undefined;
const points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one const points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2 !Array.isArray(element.points) || element.points.length < 2
? [pointFrom(0, 0), pointFrom(element.width, element.height)] ? [pointFrom(0, 0), pointFrom(element.width, element.height)]
@@ -493,8 +495,8 @@ export const restoreElement = (
startArrowhead, startArrowhead,
endArrowhead, endArrowhead,
points, points,
x, x: x ?? 0,
y, y: y ?? 0,
elbowed: (element as ExcalidrawArrowElement).elbowed, elbowed: (element as ExcalidrawArrowElement).elbowed,
...getSizeFromPoints(points), ...getSizeFromPoints(points),
}; };
@@ -513,12 +515,44 @@ export const restoreElement = (
}) })
: restoreElementWithProperties(element as ExcalidrawArrowElement, base); : restoreElementWithProperties(element as ExcalidrawArrowElement, base);
return { const normalizedRestoredElement = {
...restoredElement, ...restoredElement,
...LinearElementEditor.getNormalizeElementPointsAndCoords( ...LinearElementEditor.getNormalizeElementPointsAndCoords(
restoredElement, 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<LocalPoint>(0, 0),
pointFrom<LocalPoint>(100, 100),
],
isDeleted: true,
};
}
return normalizedRestoredElement;
} }
// generic elements // generic elements
@@ -666,6 +700,7 @@ export const restoreElements = <T extends ExcalidrawElement>(
const existingElementsMap = existingElements const existingElementsMap = existingElements
? arrayToMap(existingElements) ? arrayToMap(existingElements)
: null; : null;
const restoredElements = syncInvalidIndices( const restoredElements = syncInvalidIndices(
(targetElements || []).reduce((elements, element) => { (targetElements || []).reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements, // filtering out selection, which is legacy, no longer kept in elements,
@@ -762,7 +797,7 @@ export const restoreElements = <T extends ExcalidrawElement>(
} }
} }
// 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 // Need to iterate again so we have attached text nodes in elementsMap
return restoredElements.map((element) => { return restoredElements.map((element) => {
if ( if (
-2
View File
@@ -1,5 +1,3 @@
export const PRECISION = 10e-5;
// Legendre-Gauss abscissae (x values) and weights for n=24 // Legendre-Gauss abscissae (x values) and weights for n=24
// Refeerence: https://pomax.github.io/bezierinfo/legendre-gauss.html // Refeerence: https://pomax.github.io/bezierinfo/legendre-gauss.html
export const LegendreGaussN24TValues = [ export const LegendreGaussN24TValues = [