fix: Broken bindings during collab (#10537)

* fix: Broken bindings during collab

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

* move repair of non-legacy binding outside the migration branch

---------

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
2025-12-17 16:22:24 +01:00
committed by GitHub
parent becaabfa0f
commit 859207b8bc
2 changed files with 39 additions and 19 deletions

View File

@@ -746,7 +746,10 @@ class Collab extends PureComponent<CollabProps, CollabState> {
): ReconciledExcalidrawElement[] => { ): ReconciledExcalidrawElement[] => {
const localElements = this.getSceneElementsIncludingDeleted(); const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState(); const appState = this.excalidrawAPI.getAppState();
const restoredRemoteElements = restoreElements(remoteElements, null); const restoredRemoteElements = restoreElements(
remoteElements,
this.excalidrawAPI.getSceneElementsMapIncludingDeleted(),
);
const reconciledElements = reconcileElements( const reconciledElements = reconcileElements(
localElements, localElements,
restoredRemoteElements as RemoteExcalidrawElement[], restoredRemoteElements as RemoteExcalidrawElement[],

View File

@@ -56,6 +56,7 @@ import type { LocalPoint, Radians } from "@excalidraw/math";
import type { import type {
ElementsMap, ElementsMap,
ElementsMapOrArray,
ExcalidrawArrowElement, ExcalidrawArrowElement,
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
@@ -129,7 +130,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
const repairBinding = <T extends ExcalidrawArrowElement>( const repairBinding = <T extends ExcalidrawArrowElement>(
element: T, element: T,
binding: FixedPointBinding | null, binding: FixedPointBinding | null,
elementsMap: Readonly<ElementsMap>, targetElementsMap: Readonly<ElementsMap>,
localElementsMap: Readonly<ElementsMap> | null | undefined,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
): FixedPointBinding | null => { ): FixedPointBinding | null => {
if (!binding) { if (!binding) {
@@ -148,18 +150,27 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
return fixedPointBinding; return fixedPointBinding;
} }
const boundElement = // Fallback if the bound element is missing but the binding is at least
(elementsMap.get(binding.elementId) as ExcalidrawBindableElement) || // looking like a valid one shape-wise
undefined; if (binding.mode && binding.fixedPoint && binding.elementId) {
if (boundElement) { return {
if (binding.mode) { elementId: binding.elementId,
return { mode: binding.mode,
elementId: binding.elementId, fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
mode: binding.mode || "orbit", } as FixedPointBinding | null;
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]), }
} as FixedPointBinding | null;
}
const targetBoundElement =
(targetElementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
undefined;
const boundElement =
targetBoundElement ||
(localElementsMap?.get(binding.elementId) as ExcalidrawBindableElement) ||
undefined;
const elementsMap = targetBoundElement ? targetElementsMap : localElementsMap;
// migrating legacy focus point bindings
if (boundElement && elementsMap) {
const p = LinearElementEditor.getPointAtIndexGlobalCoordinates( const p = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
startOrEnd === "start" ? 0 : element.points.length - 1, startOrEnd === "start" ? 0 : element.points.length - 1,
@@ -193,6 +204,8 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
}; };
} }
console.error(`could not repair binding for element`);
return null; return null;
}; };
@@ -284,7 +297,8 @@ const restoreElementWithProperties = <
export const restoreElement = ( export const restoreElement = (
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>, element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
elementsMap: Readonly<ElementsMap>, targetElementsMap: Readonly<ElementsMap>,
localElementsMap: Readonly<ElementsMap> | null | undefined,
opts?: { opts?: {
deleteInvisibleElements?: boolean; deleteInvisibleElements?: boolean;
}, },
@@ -405,13 +419,15 @@ export const restoreElement = (
startBinding: repairBinding( startBinding: repairBinding(
element as ExcalidrawArrowElement, element as ExcalidrawArrowElement,
element.startBinding, element.startBinding,
elementsMap, targetElementsMap,
localElementsMap,
"start", "start",
), ),
endBinding: repairBinding( endBinding: repairBinding(
element as ExcalidrawArrowElement, element as ExcalidrawArrowElement,
element.endBinding, element.endBinding,
elementsMap, targetElementsMap,
localElementsMap,
"end", "end",
), ),
startArrowhead, startArrowhead,
@@ -575,7 +591,7 @@ const repairFrameMembership = (
export const restoreElements = ( export const restoreElements = (
targetElements: ImportedDataState["elements"], targetElements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */ /** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined, localElements: Readonly<ElementsMapOrArray> | null | undefined,
opts?: opts?:
| { | {
refreshDimensions?: boolean; refreshDimensions?: boolean;
@@ -586,7 +602,7 @@ export const restoreElements = (
): OrderedExcalidrawElement[] => { ): OrderedExcalidrawElement[] => {
// used to detect duplicate top-level element ids // used to detect duplicate top-level element ids
const existingIds = new Set<string>(); const existingIds = new Set<string>();
const elementsMap = arrayToMap(targetElements || []); const targetElementsMap = arrayToMap(targetElements || []);
const localElementsMap = localElements ? arrayToMap(localElements) : null; const localElementsMap = localElements ? arrayToMap(localElements) : null;
const restoredElements = syncInvalidIndices( const restoredElements = syncInvalidIndices(
(targetElements || []).reduce((elements, element) => { (targetElements || []).reduce((elements, element) => {
@@ -598,7 +614,8 @@ export const restoreElements = (
let migratedElement: ExcalidrawElement | null = restoreElement( let migratedElement: ExcalidrawElement | null = restoreElement(
element, element,
elementsMap, targetElementsMap,
localElementsMap,
{ {
deleteInvisibleElements: opts?.deleteInvisibleElements, deleteInvisibleElements: opts?.deleteInvisibleElements,
}, },