fix: make arrow binding area adapt to zoom levels (#8927)

* make binding area adapt to zoom

* revert stroke color

* normalize binding gap

* reduce normalized gap
pull/8896/merge
Ryan Di 1 month ago committed by GitHub
parent 873698a1a2
commit 1e3399eac8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -161,6 +161,7 @@ export const actionDeleteSelected = register({
element, element,
selectedPointsIndices, selectedPointsIndices,
elementsMap, elementsMap,
appState.zoom,
); );
return { return {

@ -153,6 +153,7 @@ const flipElements = (
app.scene, app.scene,
isBindingEnabled(appState), isBindingEnabled(appState),
[], [],
appState.zoom,
); );
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

@ -1591,6 +1591,7 @@ export const actionChangeArrowType = register({
tupleToCoors(startGlobalPoint), tupleToCoors(startGlobalPoint),
elements, elements,
elementsMap, elementsMap,
appState.zoom,
true, true,
); );
const endHoveredElement = const endHoveredElement =
@ -1599,6 +1600,7 @@ export const actionChangeArrowType = register({
tupleToCoors(endGlobalPoint), tupleToCoors(endGlobalPoint),
elements, elements,
elementsMap, elementsMap,
appState.zoom,
true, true,
); );
const startElement = startHoveredElement const startElement = startHoveredElement

@ -3215,6 +3215,10 @@ class App extends React.Component<AppProps, AppState> {
), ),
), ),
[el.points[0], el.points[el.points.length - 1]], [el.points[0], el.points[el.points.length - 1]],
undefined,
{
zoom: this.state.zoom,
},
), ),
}; };
} }
@ -4372,6 +4376,7 @@ class App extends React.Component<AppProps, AppState> {
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), { updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
zoom: this.state.zoom,
}); });
}); });
@ -4381,6 +4386,7 @@ class App extends React.Component<AppProps, AppState> {
(element) => element.id !== elbowArrow?.id || step !== 0, (element) => element.id !== elbowArrow?.id || step !== 0,
), ),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom,
), ),
}); });
@ -4596,6 +4602,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene, this.scene,
isBindingEnabled(this.state), isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.state.zoom,
); );
this.setState({ suggestedBindings: [] }); this.setState({ suggestedBindings: [] });
} }
@ -5854,6 +5861,7 @@ class App extends React.Component<AppProps, AppState> {
{ {
isDragging: true, isDragging: true,
informMutation: false, informMutation: false,
zoom: this.state.zoom,
}, },
); );
} else { } else {
@ -7401,6 +7409,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin, pointerDownState.origin,
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom,
); );
this.setState({ this.setState({
@ -7698,6 +7707,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin, pointerDownState.origin,
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom,
isElbowArrow(element), isElbowArrow(element),
); );
@ -8276,6 +8286,7 @@ class App extends React.Component<AppProps, AppState> {
suggestedBindings: getSuggestedBindingsForArrows( suggestedBindings: getSuggestedBindingsForArrows(
selectedElements, selectedElements,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom,
), ),
}); });
} }
@ -8444,6 +8455,7 @@ class App extends React.Component<AppProps, AppState> {
{ {
isDragging: true, isDragging: true,
informMutation: false, informMutation: false,
zoom: this.state.zoom,
}, },
); );
} else if (points.length === 2) { } else if (points.length === 2) {
@ -9408,6 +9420,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene, this.scene,
isBindingEnabled(this.state), isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.state.zoom,
); );
} }
@ -9900,6 +9913,7 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords, pointerCoords,
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom,
); );
this.setState({ this.setState({
suggestedBindings: suggestedBindings:
@ -9928,6 +9942,7 @@ class App extends React.Component<AppProps, AppState> {
coords, coords,
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom,
isArrowElement(linearElement) && isElbowArrow(linearElement), isArrowElement(linearElement) && isElbowArrow(linearElement),
); );
if ( if (
@ -10569,6 +10584,7 @@ class App extends React.Component<AppProps, AppState> {
const suggestedBindings = getSuggestedBindingsForArrows( const suggestedBindings = getSuggestedBindingsForArrows(
selectedElements, selectedElements,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state.zoom,
); );
const elementsToHighlight = new Set<ExcalidrawElement>(); const elementsToHighlight = new Set<ExcalidrawElement>();

@ -300,6 +300,7 @@ export const updateBindings = (
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
zoom?: AppState["zoom"];
}, },
) => { ) => {
if (isLinearElement(latestElement)) { if (isLinearElement(latestElement)) {
@ -310,6 +311,7 @@ export const updateBindings = (
scene, scene,
true, true,
[], [],
options?.zoom,
); );
} else { } else {
updateBoundElements(latestElement, elementsMap, options); updateBoundElements(latestElement, elementsMap, options);

@ -95,7 +95,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 35, "height": 33.519031369643244,
"id": Any<String>, "id": Any<String>,
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -109,8 +109,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
0.5, 0.5,
], ],
[ [
394.5, 382.47606040672997,
34.5, 34.019031369643244,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -128,9 +128,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 4, "version": 7,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 395, "width": 381.97606040672997,
"x": 247, "x": 247,
"y": 420, "y": 420,
} }
@ -167,7 +167,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
0, 0,
], ],
[ [
399.5, 389.5,
0, 0,
], ],
], ],
@ -186,10 +186,10 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 4, "version": 6,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 400, "width": 390,
"x": 227, "x": 237,
"y": 450, "y": 450,
} }
`; `;
@ -319,7 +319,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"verticalAlign": "top", "verticalAlign": "top",
"width": 100, "width": 100,
"x": 560, "x": 560,
"y": 226.5, "y": 236.95454545454544,
} }
`; `;
@ -339,13 +339,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"endBinding": { "endBinding": {
"elementId": "text-2", "elementId": "text-2",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 1.625925925925924,
"gap": 205, "gap": 14,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 18.278619528619487,
"id": Any<String>, "id": Any<String>,
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -356,11 +356,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"points": [ "points": [
[ [
0.5, 0.5,
0, -0.5,
], ],
[ [
99.5, 357.2037037037038,
0, -17.778619528619487,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -378,11 +378,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 4, "version": 6,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 100, "width": 357.7037037037038,
"x": 255, "x": 171,
"y": 239, "y": 249.45454545454544,
} }
`; `;
@ -482,7 +482,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 4, "version": 6,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 100, "width": 100,
"x": 255, "x": 255,
@ -660,7 +660,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 4, "version": 6,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 100, "width": 100,
"x": 255, "x": 255,
@ -1505,7 +1505,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
0, 0,
], ],
[ [
272.485, 270.98528125,
0, 0,
], ],
], ],
@ -1526,10 +1526,10 @@ exports[`Test Transform > should transform the elements correctly when linear el
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 4, "version": 7,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 272.985, "width": 270.48528125,
"x": 111.262, "x": 112.76171875,
"y": 57, "y": 57,
} }
`; `;
@ -1587,11 +1587,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 4, "version": 6,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 0, "width": 0,
"x": 77.017, "x": 83.015625,
"y": 79, "y": 81.5,
} }
`; `;

@ -779,7 +779,7 @@ describe("Test Transform", () => {
elementId: "rect-1", elementId: "rect-1",
fixedPoint: null, fixedPoint: null,
focus: 0, focus: 0,
gap: 205, gap: 14,
}); });
expect(rect.boundElements).toStrictEqual([ expect(rect.boundElements).toStrictEqual([
{ {

@ -40,7 +40,6 @@ import {
isBoundToContainer, isBoundToContainer,
isElbowArrow, isElbowArrow,
isFixedPointBinding, isFixedPointBinding,
isFrameLikeElement,
isLinearElement, isLinearElement,
isRectangularElement, isRectangularElement,
isTextElement, isTextElement,
@ -97,6 +96,8 @@ export const isBindingEnabled = (appState: AppState): boolean => {
}; };
export const FIXED_BINDING_DISTANCE = 5; export const FIXED_BINDING_DISTANCE = 5;
export const BINDING_HIGHLIGHT_THICKNESS = 10;
export const BINDING_HIGHLIGHT_OFFSET = 4;
const getNonDeletedElements = ( const getNonDeletedElements = (
scene: Scene, scene: Scene,
@ -213,6 +214,7 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
edge: "start" | "end", edge: "start" | "end",
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): NonDeleted<ExcalidrawElement> | null => { ): NonDeleted<ExcalidrawElement> | null => {
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap); const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
const elementId = const elementId =
@ -223,7 +225,7 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
const element = elementsMap.get(elementId); const element = elementsMap.get(elementId);
if ( if (
isBindableElement(element) && isBindableElement(element) &&
bindingBorderTest(element, coors, elementsMap) bindingBorderTest(element, coors, elementsMap, zoom)
) { ) {
return element; return element;
} }
@ -235,12 +237,14 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
const getOriginalBindingsIfStillCloseToArrowEnds = ( const getOriginalBindingsIfStillCloseToArrowEnds = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): (NonDeleted<ExcalidrawElement> | null)[] => ): (NonDeleted<ExcalidrawElement> | null)[] =>
["start", "end"].map((edge) => ["start", "end"].map((edge) =>
getOriginalBindingIfStillCloseOfLinearElementEdge( getOriginalBindingIfStillCloseOfLinearElementEdge(
linearElement, linearElement,
edge as "start" | "end", edge as "start" | "end",
elementsMap, elementsMap,
zoom,
), ),
); );
@ -250,6 +254,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
draggingPoints: readonly number[], draggingPoints: readonly number[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
zoom?: AppState["zoom"],
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => { ): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
const startIdx = 0; const startIdx = 0;
const endIdx = selectedElement.points.length - 1; const endIdx = selectedElement.points.length - 1;
@ -262,6 +267,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
"start", "start",
elementsMap, elementsMap,
elements, elements,
zoom,
) )
: null // If binding is disabled and start is dragged, break all binds : null // If binding is disabled and start is dragged, break all binds
: // We have to update the focus and gap of the binding, so let's rebind : // We have to update the focus and gap of the binding, so let's rebind
@ -270,6 +276,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
"start", "start",
elementsMap, elementsMap,
elements, elements,
zoom,
); );
const end = endDragged const end = endDragged
? isBindingEnabled ? isBindingEnabled
@ -278,6 +285,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
"end", "end",
elementsMap, elementsMap,
elements, elements,
zoom,
) )
: null // If binding is disabled and end is dragged, break all binds : null // If binding is disabled and end is dragged, break all binds
: // We have to update the focus and gap of the binding, so let's rebind : // We have to update the focus and gap of the binding, so let's rebind
@ -286,6 +294,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
"end", "end",
elementsMap, elementsMap,
elements, elements,
zoom,
); );
return [start, end]; return [start, end];
@ -296,10 +305,12 @@ const getBindingStrategyForDraggingArrowOrJoints = (
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
isBindingEnabled: boolean, isBindingEnabled: boolean,
zoom?: AppState["zoom"],
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => { ): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds( const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
selectedElement, selectedElement,
elementsMap, elementsMap,
zoom,
); );
const start = startIsClose const start = startIsClose
? isBindingEnabled ? isBindingEnabled
@ -308,6 +319,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
"start", "start",
elementsMap, elementsMap,
elements, elements,
zoom,
) )
: null : null
: null; : null;
@ -318,6 +330,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
"end", "end",
elementsMap, elementsMap,
elements, elements,
zoom,
) )
: null : null
: null; : null;
@ -332,6 +345,7 @@ export const bindOrUnbindLinearElements = (
scene: Scene, scene: Scene,
isBindingEnabled: boolean, isBindingEnabled: boolean,
draggingPoints: readonly number[] | null, draggingPoints: readonly number[] | null,
zoom?: AppState["zoom"],
): void => { ): void => {
selectedElements.forEach((selectedElement) => { selectedElements.forEach((selectedElement) => {
const [start, end] = draggingPoints?.length const [start, end] = draggingPoints?.length
@ -342,6 +356,7 @@ export const bindOrUnbindLinearElements = (
draggingPoints ?? [], draggingPoints ?? [],
elementsMap, elementsMap,
elements, elements,
zoom,
) )
: // The arrow itself (the shaft) or the inner joins are dragged : // The arrow itself (the shaft) or the inner joins are dragged
getBindingStrategyForDraggingArrowOrJoints( getBindingStrategyForDraggingArrowOrJoints(
@ -349,6 +364,7 @@ export const bindOrUnbindLinearElements = (
elementsMap, elementsMap,
elements, elements,
isBindingEnabled, isBindingEnabled,
zoom,
); );
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene); bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
@ -358,6 +374,7 @@ export const bindOrUnbindLinearElements = (
export const getSuggestedBindingsForArrows = ( export const getSuggestedBindingsForArrows = (
selectedElements: NonDeleted<ExcalidrawElement>[], selectedElements: NonDeleted<ExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom: AppState["zoom"],
): SuggestedBinding[] => { ): SuggestedBinding[] => {
// HOT PATH: Bail out if selected elements list is too large // HOT PATH: Bail out if selected elements list is too large
if (selectedElements.length > 50) { if (selectedElements.length > 50) {
@ -368,7 +385,7 @@ export const getSuggestedBindingsForArrows = (
selectedElements selectedElements
.filter(isLinearElement) .filter(isLinearElement)
.flatMap((element) => .flatMap((element) =>
getOriginalBindingsIfStillCloseToArrowEnds(element, elementsMap), getOriginalBindingsIfStillCloseToArrowEnds(element, elementsMap, zoom),
) )
.filter( .filter(
(element): element is NonDeleted<ExcalidrawBindableElement> => (element): element is NonDeleted<ExcalidrawBindableElement> =>
@ -406,6 +423,7 @@ export const maybeBindLinearElement = (
pointerCoords, pointerCoords,
elements, elements,
elementsMap, elementsMap,
appState.zoom,
isElbowArrow(linearElement) && isElbowArrow(linearElement), isElbowArrow(linearElement) && isElbowArrow(linearElement),
); );
@ -422,6 +440,26 @@ export const maybeBindLinearElement = (
} }
}; };
const normalizePointBinding = (
binding: { focus: number; gap: number },
hoveredElement: ExcalidrawBindableElement,
) => {
let gap = binding.gap;
const maxGap = maxBindingGap(
hoveredElement,
hoveredElement.width,
hoveredElement.height,
);
if (gap > maxGap) {
gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
}
return {
...binding,
gap,
};
};
export const bindLinearElement = ( export const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
@ -433,11 +471,14 @@ export const bindLinearElement = (
} }
const binding: PointBinding = { const binding: PointBinding = {
elementId: hoveredElement.id, elementId: hoveredElement.id,
...calculateFocusAndGap( ...normalizePointBinding(
linearElement, calculateFocusAndGap(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
hoveredElement, hoveredElement,
startOrEnd,
elementsMap,
), ),
...(isElbowArrow(linearElement) ...(isElbowArrow(linearElement)
? calculateFixedPointForElbowArrowBinding( ? calculateFixedPointForElbowArrowBinding(
@ -462,6 +503,12 @@ export const bindLinearElement = (
}), }),
}); });
} }
// update bound elements to make sure the binding tips are in sync with
// the normalized gap from above
if (!isElbowArrow(linearElement)) {
updateBoundElements(hoveredElement, elementsMap);
}
}; };
// Don't bind both ends of a simple segment // Don't bind both ends of a simple segment
@ -514,6 +561,7 @@ export const getHoveredElementForBinding = (
}, },
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
fullShape?: boolean, fullShape?: boolean,
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement> | null => {
const hoveredElement = getElementAtPosition( const hoveredElement = getElementAtPosition(
@ -524,11 +572,13 @@ export const getHoveredElementForBinding = (
element, element,
pointerCoords, pointerCoords,
elementsMap, elementsMap,
zoom,
// disable fullshape snapping for frame elements so we // disable fullshape snapping for frame elements so we
// can bind to frame children // can bind to frame children
fullShape && !isFrameLikeElement(element), fullShape,
), ),
); );
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null; return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
}; };
@ -578,9 +628,11 @@ export const updateBoundElements = (
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
changedElements?: Map<string, OrderedExcalidrawElement>; changedElements?: Map<string, OrderedExcalidrawElement>;
zoom?: AppState["zoom"];
}, },
) => { ) => {
const { newSize, simultaneouslyUpdated, changedElements } = options ?? {}; const { newSize, simultaneouslyUpdated, changedElements, zoom } =
options ?? {};
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
@ -670,6 +722,7 @@ export const updateBoundElements = (
}, },
{ {
changedElements, changedElements,
zoom,
}, },
); );
@ -703,6 +756,7 @@ export const getHeadingForElbowArrowSnap = (
aabb: Bounds | undefined | null, aabb: Bounds | undefined | null,
elementsMap: ElementsMap, elementsMap: ElementsMap,
origPoint: GlobalPoint, origPoint: GlobalPoint,
zoom?: AppState["zoom"],
): Heading => { ): Heading => {
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p)); const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
@ -714,6 +768,7 @@ export const getHeadingForElbowArrowSnap = (
origPoint, origPoint,
bindableElement, bindableElement,
elementsMap, elementsMap,
zoom,
); );
if (!distance) { if (!distance) {
@ -737,6 +792,7 @@ const getDistanceForBinding = (
point: Readonly<GlobalPoint>, point: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement, bindableElement: ExcalidrawBindableElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
zoom?: AppState["zoom"],
) => { ) => {
const distance = distanceToBindableElement( const distance = distanceToBindableElement(
bindableElement, bindableElement,
@ -747,6 +803,7 @@ const getDistanceForBinding = (
bindableElement, bindableElement,
bindableElement.width, bindableElement.width,
bindableElement.height, bindableElement.height,
zoom,
); );
return distance > bindDistance ? null : distance; return distance > bindDistance ? null : distance;
@ -1174,11 +1231,13 @@ const getElligibleElementForBindingElement = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
zoom?: AppState["zoom"],
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement> | null => {
return getHoveredElementForBinding( return getHoveredElementForBinding(
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap), getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
elements, elements,
elementsMap, elementsMap,
zoom,
); );
}; };
@ -1341,9 +1400,11 @@ export const bindingBorderTest = (
element: NonDeleted<ExcalidrawBindableElement>, element: NonDeleted<ExcalidrawBindableElement>,
{ x, y }: { x: number; y: number }, { x, y }: { x: number; y: number },
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
fullShape?: boolean, fullShape?: boolean,
): boolean => { ): boolean => {
const threshold = maxBindingGap(element, element.width, element.height); const threshold = maxBindingGap(element, element.width, element.height, zoom);
const shape = getElementShape(element, elementsMap); const shape = getElementShape(element, elementsMap);
return ( return (
isPointOnShape(pointFrom(x, y), shape, threshold) || isPointOnShape(pointFrom(x, y), shape, threshold) ||
@ -1356,12 +1417,21 @@ export const maxBindingGap = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementWidth: number, elementWidth: number,
elementHeight: number, elementHeight: number,
zoom?: AppState["zoom"],
): number => { ): number => {
const zoomValue = zoom?.value && zoom.value < 1 ? zoom.value : 1;
// Aligns diamonds with rectangles // Aligns diamonds with rectangles
const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1; const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1;
const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight); const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight);
// We make the bindable boundary bigger for bigger elements
return Math.max(16, Math.min(0.25 * smallerDimension, 32)); return Math.max(
16,
// bigger bindable boundary for bigger elements
Math.min(0.25 * smallerDimension, 32),
// keep in sync with the zoomed highlight
BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET,
);
}; };
export const distanceToBindableElement = ( export const distanceToBindableElement = (

@ -448,6 +448,7 @@ export class LinearElementEditor {
), ),
elements, elements,
elementsMap, elementsMap,
appState.zoom,
) )
: null; : null;
@ -787,6 +788,7 @@ export class LinearElementEditor {
scenePointer, scenePointer,
elements, elements,
elementsMap, elementsMap,
app.state.zoom,
), ),
}; };
@ -911,6 +913,7 @@ export class LinearElementEditor {
element, element,
[points.length - 1], [points.length - 1],
elementsMap, elementsMap,
app.state.zoom,
); );
} }
return { return {
@ -964,6 +967,7 @@ export class LinearElementEditor {
element, element,
[{ point: newPoint }], [{ point: newPoint }],
elementsMap, elementsMap,
app.state.zoom,
); );
} }
return { return {
@ -1218,6 +1222,7 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
pointIndices: readonly number[], pointIndices: readonly number[],
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
zoom: AppState["zoom"],
) { ) {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
@ -1260,6 +1265,7 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
targetPoints: { point: LocalPoint }[], targetPoints: { point: LocalPoint }[],
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
zoom: AppState["zoom"],
) { ) {
const offsetX = 0; const offsetX = 0;
const offsetY = 0; const offsetY = 0;
@ -1285,6 +1291,7 @@ export class LinearElementEditor {
options?: { options?: {
changedElements?: Map<string, OrderedExcalidrawElement>; changedElements?: Map<string, OrderedExcalidrawElement>;
isDragging?: boolean; isDragging?: boolean;
zoom?: AppState["zoom"];
}, },
) { ) {
const { points } = element; const { points } = element;
@ -1337,6 +1344,7 @@ export class LinearElementEditor {
false, false,
), ),
changedElements: options?.changedElements, changedElements: options?.changedElements,
zoom: options?.zoom,
}, },
); );
} }
@ -1451,6 +1459,7 @@ export class LinearElementEditor {
options?: { options?: {
changedElements?: Map<string, OrderedExcalidrawElement>; changedElements?: Map<string, OrderedExcalidrawElement>;
isDragging?: boolean; isDragging?: boolean;
zoom?: AppState["zoom"];
}, },
) { ) {
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
@ -1487,6 +1496,7 @@ export class LinearElementEditor {
bindings, bindings,
{ {
isDragging: options?.isDragging, isDragging: options?.isDragging,
zoom: options?.zoom,
}, },
); );
} else { } else {

@ -14,6 +14,7 @@ import {
import BinaryHeap from "../binaryheap"; import BinaryHeap from "../binaryheap";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import { aabbForElement, pointInsideBounds } from "../shapes"; import { aabbForElement, pointInsideBounds } from "../shapes";
import type { AppState } from "../types";
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils"; import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
import { import {
bindPointToSnapToElementOutline, bindPointToSnapToElementOutline,
@ -79,6 +80,7 @@ export const mutateElbowArrow = (
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
informMutation?: boolean; informMutation?: boolean;
zoom?: AppState["zoom"];
}, },
) => { ) => {
const update = updateElbowArrow( const update = updateElbowArrow(
@ -112,6 +114,7 @@ export const updateElbowArrow = (
isDragging?: boolean; isDragging?: boolean;
disableBinding?: boolean; disableBinding?: boolean;
informMutation?: boolean; informMutation?: boolean;
zoom?: AppState["zoom"];
}, },
): ElementUpdate<ExcalidrawElbowArrowElement> | null => { ): ElementUpdate<ExcalidrawElbowArrowElement> | null => {
const origStartGlobalPoint: GlobalPoint = pointTranslate( const origStartGlobalPoint: GlobalPoint = pointTranslate(
@ -136,7 +139,12 @@ export const updateElbowArrow = (
arrow.endBinding && arrow.endBinding &&
getBindableElementForId(arrow.endBinding.elementId, elementsMap); getBindableElementForId(arrow.endBinding.elementId, elementsMap);
const [hoveredStartElement, hoveredEndElement] = options?.isDragging const [hoveredStartElement, hoveredEndElement] = options?.isDragging
? getHoveredElements(origStartGlobalPoint, origEndGlobalPoint, elementsMap) ? getHoveredElements(
origStartGlobalPoint,
origEndGlobalPoint,
elementsMap,
options?.zoom,
)
: [startElement, endElement]; : [startElement, endElement];
const startGlobalPoint = getGlobalPoint( const startGlobalPoint = getGlobalPoint(
arrow.startBinding?.fixedPoint, arrow.startBinding?.fixedPoint,
@ -1072,6 +1080,7 @@ const getHoveredElements = (
origStartGlobalPoint: GlobalPoint, origStartGlobalPoint: GlobalPoint,
origEndGlobalPoint: GlobalPoint, origEndGlobalPoint: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
zoom?: AppState["zoom"],
) => { ) => {
// TODO: Might be a performance bottleneck and the Map type // TODO: Might be a performance bottleneck and the Map type
// remembers the insertion order anyway... // remembers the insertion order anyway...
@ -1084,12 +1093,14 @@ const getHoveredElements = (
tupleToCoors(origStartGlobalPoint), tupleToCoors(origStartGlobalPoint),
elements, elements,
nonDeletedSceneElementsMap, nonDeletedSceneElementsMap,
zoom,
true, true,
), ),
getHoveredElementForBinding( getHoveredElementForBinding(
tupleToCoors(origEndGlobalPoint), tupleToCoors(origEndGlobalPoint),
elements, elements,
nonDeletedSceneElementsMap, nonDeletedSceneElementsMap,
zoom,
true, true,
), ),
]; ];

@ -43,7 +43,11 @@ import type {
SuggestedBinding, SuggestedBinding,
SuggestedPointBinding, SuggestedPointBinding,
} from "../element/binding"; } from "../element/binding";
import { maxBindingGap } from "../element/binding"; import {
BINDING_HIGHLIGHT_OFFSET,
BINDING_HIGHLIGHT_THICKNESS,
maxBindingGap,
} from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { import {
bootstrapCanvas, bootstrapCanvas,
@ -217,17 +221,18 @@ const renderBindingHighlightForBindableElement = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
zoom: InteractiveCanvasAppState["zoom"],
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const width = x2 - x1; const width = x2 - x1;
const height = y2 - y1; const height = y2 - y1;
const thickness = 10;
// So that we don't overlap the element itself
const strokeOffset = 4;
context.strokeStyle = "rgba(0,0,0,.05)"; context.strokeStyle = "rgba(0,0,0,.05)";
context.lineWidth = thickness - strokeOffset; // When zooming out, make line width greater for visibility
const padding = strokeOffset / 2 + thickness / 2; const zoomValue = zoom.value < 1 ? zoom.value : 1;
context.lineWidth = BINDING_HIGHLIGHT_THICKNESS / zoomValue;
// To ensure the binding highlight doesn't overlap the element itself
const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET;
const radius = getCornerRadius( const radius = getCornerRadius(
Math.min(element.width, element.height), Math.min(element.width, element.height),
@ -285,6 +290,7 @@ const renderBindingHighlightForSuggestedPointBinding = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
suggestedBinding: SuggestedPointBinding, suggestedBinding: SuggestedPointBinding,
elementsMap: ElementsMap, elementsMap: ElementsMap,
zoom: InteractiveCanvasAppState["zoom"],
) => { ) => {
const [element, startOrEnd, bindableElement] = suggestedBinding; const [element, startOrEnd, bindableElement] = suggestedBinding;
@ -292,6 +298,7 @@ const renderBindingHighlightForSuggestedPointBinding = (
bindableElement, bindableElement,
bindableElement.width, bindableElement.width,
bindableElement.height, bindableElement.height,
zoom,
); );
context.strokeStyle = "rgba(0,0,0,0)"; context.strokeStyle = "rgba(0,0,0,0)";
@ -390,7 +397,7 @@ const renderBindingHighlight = (
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
renderHighlight(context, suggestedBinding as any, elementsMap); renderHighlight(context, suggestedBinding as any, elementsMap, appState.zoom);
context.restore(); context.restore();
}; };

@ -197,7 +197,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 99, "height": 125,
"id": "id166", "id": "id166",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -211,8 +211,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"98.20800", 125,
99, 125,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -226,9 +226,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 40, "version": 47,
"width": "98.20800", "width": 125,
"x": 1, "x": 0,
"y": 0, "y": 0,
} }
`; `;
@ -298,7 +298,7 @@ History {
"focus": "0.00990", "focus": "0.00990",
"gap": 1, "gap": 1,
}, },
"height": "0.98017", "height": "0.98000",
"points": [ "points": [
[ [
0, 0,
@ -306,7 +306,7 @@ History {
], ],
[ [
98, 98,
"-0.98017", "-0.98000",
], ],
], ],
"startBinding": { "startBinding": {
@ -320,10 +320,10 @@ History {
"endBinding": { "endBinding": {
"elementId": "id165", "elementId": "id165",
"fixedPoint": null, "fixedPoint": null,
"focus": "-0.02000", "focus": "-0.02040",
"gap": 1, "gap": 1,
}, },
"height": "0.00169", "height": "0.02000",
"points": [ "points": [
[ [
0, 0,
@ -331,13 +331,13 @@ History {
], ],
[ [
98, 98,
"0.00169", "0.02000",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id164", "elementId": "id164",
"fixedPoint": null, "fixedPoint": null,
"focus": "0.02000", "focus": "0.01959",
"gap": 1, "gap": 1,
}, },
}, },
@ -393,18 +393,20 @@ History {
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"height": 99, "height": 125,
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"98.20800", 125,
99, 125,
], ],
], ],
"startBinding": null, "startBinding": null,
"width": 125,
"x": 0,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -414,7 +416,7 @@ History {
"focus": "0.00990", "focus": "0.00990",
"gap": 1, "gap": 1,
}, },
"height": "0.98161", "height": "0.98000",
"points": [ "points": [
[ [
0, 0,
@ -422,7 +424,7 @@ History {
], ],
[ [
98, 98,
"-0.98161", "-0.98000",
], ],
], ],
"startBinding": { "startBinding": {
@ -431,7 +433,9 @@ History {
"focus": "0.02970", "focus": "0.02970",
"gap": 1, "gap": 1,
}, },
"y": "0.99245", "width": 98,
"x": 1,
"y": "0.99000",
}, },
}, },
"id169" => Delta { "id169" => Delta {
@ -823,9 +827,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 30, "version": 37,
"width": 0, "width": 100,
"x": 200, "x": 150,
"y": 0, "y": 0,
} }
`; `;
@ -862,6 +866,8 @@ History {
0, 0,
], ],
], ],
"width": 0,
"x": 149,
}, },
"inserted": { "inserted": {
"points": [ "points": [
@ -870,10 +876,12 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
"width": "98.00000",
"x": "1.00000",
}, },
}, },
}, },
@ -930,6 +938,8 @@ History {
], ],
], ],
"startBinding": null, "startBinding": null,
"width": 100,
"x": 150,
}, },
"inserted": { "inserted": {
"endBinding": { "endBinding": {
@ -954,6 +964,8 @@ History {
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"width": 0,
"x": 149,
}, },
}, },
}, },
@ -2363,9 +2375,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": 498, "width": 498,
"x": 1, "x": "1.00000",
"y": 0, "y": 0,
} }
`; `;
@ -2504,7 +2516,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -2523,8 +2535,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "98.00000",
"x": 0, "x": 1,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -15167,9 +15179,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": "98.00000", "width": "98.00000",
"x": 1, "x": "1.00000",
"y": 0, "y": 0,
} }
`; `;
@ -15208,7 +15220,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -15221,7 +15233,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -15517,7 +15529,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -15536,8 +15548,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "98.00000",
"x": 0, "x": 1,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -15866,9 +15878,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": "98.00000", "width": "98.00000",
"x": 1, "x": "1.00000",
"y": 0, "y": 0,
} }
`; `;
@ -16140,7 +16152,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -16159,8 +16171,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "98.00000",
"x": 0, "x": 1,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -16489,9 +16501,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": "98.00000", "width": "98.00000",
"x": 1, "x": "1.00000",
"y": 0, "y": 0,
} }
`; `;
@ -16763,7 +16775,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -16782,8 +16794,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "98.00000",
"x": 0, "x": 1,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -17110,9 +17122,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": "98.00000", "width": "98.00000",
"x": 1, "x": "1.00000",
"y": 0, "y": 0,
} }
`; `;
@ -17168,7 +17180,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -17186,7 +17198,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -17455,7 +17467,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -17474,8 +17486,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "98.00000",
"x": 0, "x": 1,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -17828,9 +17840,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 13,
"width": "98.00000", "width": "98.00000",
"x": 1, "x": "1.00000",
"y": 0, "y": 0,
} }
`; `;
@ -17901,7 +17913,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -17920,7 +17932,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -18189,7 +18201,7 @@ History {
0, 0,
], ],
[ [
100, "98.00000",
0, 0,
], ],
], ],
@ -18208,8 +18220,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "98.00000",
"x": 0, "x": 1,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {

@ -173,7 +173,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 7, "version": 7,
"versionNonce": 745419401, "versionNonce": 2066753033,
"width": 300, "width": 300,
"x": 201, "x": 201,
"y": 2, "y": 2,
@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 15,
"versionNonce": 1996028265, "versionNonce": 271613161,
"width": 81, "width": 81,
"x": 110, "x": 110,
"y": 50, "y": 50,

@ -4785,21 +4785,17 @@ describe("history", () => {
expect.objectContaining({ id: rect2.id, boundElements: [] }), expect.objectContaining({ id: rect2.id, boundElements: [] }),
expect.objectContaining({ expect.objectContaining({
id: arrowId, id: arrowId,
points: [
[0, 0],
[100, 0],
],
startBinding: expect.objectContaining({ startBinding: expect.objectContaining({
elementId: rect1.id, elementId: rect1.id,
fixedPoint: null, fixedPoint: null,
focus: expect.toBeNonNaNNumber(), focus: 0,
gap: expect.toBeNonNaNNumber(), gap: 1,
}), }),
endBinding: expect.objectContaining({ endBinding: expect.objectContaining({
elementId: rect2.id, elementId: rect2.id,
fixedPoint: null, fixedPoint: null,
focus: expect.toBeNonNaNNumber(), focus: 0,
gap: expect.toBeNonNaNNumber(), gap: 1,
}), }),
isDeleted: true, isDeleted: true,
}), }),

Loading…
Cancel
Save