fix(editor): Hardened fixed point and bound element parsing in restore (#10816)

* fix: Reinforce fixedPoint restore

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Even more hardened boundElement in restore

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Extract constant

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Remove superfluous check from restore

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Remove non-needed code path

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: More robust number test for fixedPoint parsing

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Validate bindings for element being parsed

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* unrelated type safety

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács
2026-02-23 19:22:27 +00:00
committed by GitHub
co-authored by dwelle
parent b0404b10b6
commit 7ea3229e17
2 changed files with 51 additions and 19 deletions
+24 -8
View File
@@ -2447,21 +2447,37 @@ export const getArrowLocalFixedPoints = (
]; ];
}; };
export const normalizeFixedPoint = <T extends FixedPoint | null>( export const isFixedPoint = (
fixedPoint: any,
): fixedPoint is FixedPointBinding["fixedPoint"] => {
return (
Array.isArray(fixedPoint) &&
fixedPoint.length === 2 &&
fixedPoint.every((coord) => Number.isFinite(coord))
);
};
export const normalizeFixedPoint = <T extends FixedPoint>(
fixedPoint: T, fixedPoint: T,
): T extends null ? null : FixedPoint => { ): FixedPoint => {
if (!isFixedPoint(fixedPoint)) {
return [0.5001, 0.5001];
}
const EPSILON = 0.0001;
// Do not allow a precise 0.5 for fixed point ratio // Do not allow a precise 0.5 for fixed point ratio
// to avoid jumping arrow heading due to floating point imprecision // to avoid jumping arrow heading due to floating point imprecision
if ( if (
fixedPoint && Math.abs(fixedPoint[0] - 0.5) < EPSILON ||
(Math.abs(fixedPoint[0] - 0.5) < 0.0001 || Math.abs(fixedPoint[1] - 0.5) < EPSILON
Math.abs(fixedPoint[1] - 0.5) < 0.0001)
) { ) {
return fixedPoint.map((ratio) => return fixedPoint.map((ratio) =>
Math.abs(ratio - 0.5) < 0.0001 ? 0.5001 : ratio, Math.abs(ratio - 0.5) < EPSILON ? 0.5001 : ratio,
) as T extends null ? null : FixedPoint; ) as FixedPoint;
} }
return fixedPoint as any as T extends null ? null : FixedPoint;
return fixedPoint;
}; };
type Side = type Side =
+27 -11
View File
@@ -155,7 +155,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
| ExcalidrawElbowArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"]
| ExcalidrawElbowArrowElement["endBinding"] = { | ExcalidrawElbowArrowElement["endBinding"] = {
...binding, ...binding,
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]), fixedPoint: normalizeFixedPoint(binding.fixedPoint),
mode: binding.mode || "orbit", mode: binding.mode || "orbit",
}; };
@@ -176,7 +176,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
return { return {
elementId: binding.elementId, elementId: binding.elementId,
mode: binding.mode, mode: binding.mode,
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]), fixedPoint: normalizeFixedPoint(binding.fixedPoint),
} as FixedPointBinding | null; } as FixedPointBinding | null;
} }
return null; return null;
@@ -185,15 +185,14 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
// binding schema v1 (legacy) -> attempt to migrate to v2 // binding schema v1 (legacy) -> attempt to migrate to v2
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const targetBoundElement = const targetBoundElement = targetElementsMap.get(binding.elementId) as
(targetElementsMap.get(binding.elementId) as ExcalidrawBindableElement) || | ExcalidrawBindableElement
undefined; | undefined;
const boundElement = const boundElement =
targetBoundElement || targetBoundElement ||
(existingElementsMap?.get( (existingElementsMap?.get(binding.elementId) as
binding.elementId, | ExcalidrawBindableElement
) as ExcalidrawBindableElement) || | undefined);
undefined;
const elementsMap = targetBoundElement const elementsMap = targetBoundElement
? targetElementsMap ? targetElementsMap
: existingElementsMap; : existingElementsMap;
@@ -208,11 +207,28 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
const mode = isPointInElement(p, boundElement, elementsMap) const mode = isPointInElement(p, boundElement, elementsMap)
? "inside" ? "inside"
: "orbit"; : "orbit";
const safeElement = {
...element,
startBinding: element.startBinding?.elementId
? {
...element.startBinding,
mode,
fixedPoint: normalizeFixedPoint(element.startBinding.fixedPoint),
}
: null,
endBinding: element.endBinding?.elementId
? {
...element.endBinding,
mode,
fixedPoint: normalizeFixedPoint(element.endBinding.fixedPoint),
}
: null,
};
const focusPoint = const focusPoint =
mode === "inside" mode === "inside"
? p ? p
: projectFixedPointOntoDiagonal( : projectFixedPointOntoDiagonal(
element, safeElement,
p, p,
boundElement, boundElement,
startOrEnd, startOrEnd,
@@ -220,7 +236,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
{ value: 1 as NormalizedZoomValue }, { value: 1 as NormalizedZoomValue },
) || p; ) || p;
const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding( const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding(
element, safeElement,
boundElement, boundElement,
startOrEnd, startOrEnd,
elementsMap, elementsMap,