Inline binding logic fix

This commit is contained in:
Mark Tolmacs 2025-03-29 19:06:52 +01:00
parent a79eb06939
commit 4efa6f69e5
5 changed files with 65 additions and 42 deletions

View file

@ -223,7 +223,7 @@ const bindOrUnbindLinearElementEdge = (
} }
}; };
const getOriginalBindingsIfStillCloseToArrowEnds = ( export const getOriginalBindingsIfStillCloseToArrowEnds = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
@ -418,10 +418,47 @@ export const getSuggestedBindingsForArrows = (
export const maybeBindLinearElement = ( export const maybeBindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
appState: AppState, appState: AppState,
pointerCoords: { x: number; y: number },
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
): void => { ): void => {
const start = tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
0,
elementsMap,
),
);
const end = tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
-1,
elementsMap,
),
);
const otherHoveredElement = getHoveredElementForBinding(
start,
elements,
elementsMap,
appState.zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
);
const hoveredElement = getHoveredElementForBinding(
end,
elements,
elementsMap,
appState.zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
);
// Inside the same element there is no binding to the shape
if (hoveredElement === otherHoveredElement) {
return;
}
if (appState.startBoundElement != null) { if (appState.startBoundElement != null) {
bindLinearElement( bindLinearElement(
linearElement, linearElement,
@ -431,15 +468,6 @@ export const maybeBindLinearElement = (
); );
} }
const hoveredElement = getHoveredElementForBinding(
pointerCoords,
elements,
elementsMap,
appState.zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
);
if (hoveredElement !== null) { if (hoveredElement !== null) {
if ( if (
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge( !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(

View file

@ -50,7 +50,6 @@ import { isBindableElement } from "./typeChecks";
import { import {
type ExcalidrawElbowArrowElement, type ExcalidrawElbowArrowElement,
type NonDeletedSceneElementsMap, type NonDeletedSceneElementsMap,
type SceneElementsMap,
} from "./types"; } from "./types";
import { aabbForElement, pointInsideBounds } from "./shapes"; import { aabbForElement, pointInsideBounds } from "./shapes";
@ -1237,6 +1236,14 @@ const getElbowArrowData = (
true, true,
true, true,
) || null; ) || null;
// Inside the same element there is no binding to the shape
if (hoveredStartElement === hoveredEndElement) {
hoveredStartElement = null;
hoveredEndElement = null;
arrow.startBinding = null;
arrow.endBinding = null;
}
} else { } else {
hoveredStartElement = arrow.startBinding hoveredStartElement = arrow.startBinding
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) || ? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
@ -1278,14 +1285,12 @@ const getElbowArrowData = (
const startHeading = getBindPointHeading( const startHeading = getBindPointHeading(
startGlobalPoint, startGlobalPoint,
endGlobalPoint, endGlobalPoint,
elementsMap,
hoveredStartElement, hoveredStartElement,
origStartGlobalPoint, origStartGlobalPoint,
); );
const endHeading = getBindPointHeading( const endHeading = getBindPointHeading(
endGlobalPoint, endGlobalPoint,
startGlobalPoint, startGlobalPoint,
elementsMap,
hoveredEndElement, hoveredEndElement,
origEndGlobalPoint, origEndGlobalPoint,
); );
@ -2257,7 +2262,6 @@ const getGlobalPoint = (
const getBindPointHeading = ( const getBindPointHeading = (
p: GlobalPoint, p: GlobalPoint,
otherPoint: GlobalPoint, otherPoint: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
hoveredElement: ExcalidrawBindableElement | null | undefined, hoveredElement: ExcalidrawBindableElement | null | undefined,
origPoint: GlobalPoint, origPoint: GlobalPoint,
): Heading => ): Heading =>

View file

@ -10,6 +10,8 @@ import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui"; import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils"; import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
import "@excalidraw/utils/test-utils";
import { getTransformHandles } from "../src/transformHandles"; import { getTransformHandles } from "../src/transformHandles";
const { h } = window; const { h } = window;
@ -510,7 +512,7 @@ describe("element binding", () => {
"allow non-binding simple (complex) arrow creation while start and end" + "allow non-binding simple (complex) arrow creation while start and end" +
" points are in the same shape", " points are in the same shape",
() => { () => {
UI.createElement("rectangle", { const rect = UI.createElement("rectangle", {
x: 0, x: 0,
y: 0, y: 0,
width: 100, width: 100,
@ -526,9 +528,10 @@ describe("element binding", () => {
expect(arrow.startBinding).toBe(null); expect(arrow.startBinding).toBe(null);
expect(arrow.endBinding).toBe(null); expect(arrow.endBinding).toBe(null);
expect(rect.boundElements).toEqual(null);
expect(arrow.points).toCloselyEqualPoints([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[95, 95], [92.2855, 92.2855],
]); ]);
const rect2 = API.createElement({ const rect2 = API.createElement({
@ -552,32 +555,35 @@ describe("element binding", () => {
expect(arrow2.startBinding).toBe(null); expect(arrow2.startBinding).toBe(null);
expect(arrow2.endBinding).toBe(null); expect(arrow2.endBinding).toBe(null);
expect(rect2.boundElements).toEqual(null);
expect(arrow2.points).toCloselyEqualPoints([ expect(arrow2.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[95, 95], [92.2855, 92.2855],
]); ]);
UI.createElement("rectangle", { const rect3 = UI.createElement("rectangle", {
x: 0, x: 0,
y: 0, y: 300,
width: 100, width: 100,
height: 100, height: 100,
}); });
const arrow3 = UI.createElement("arrow", { const arrow3 = UI.createElement("arrow", {
x: 5, x: 10,
y: 5, y: 310,
height: 95, height: 85,
width: 95, width: 84,
elbowed: true, elbowed: true,
}); });
expect(arrow3.startBinding).toBe(null); expect(arrow3.startBinding).toBe(null);
expect(arrow3.endBinding).toBe(null); expect(arrow3.endBinding).toBe(null);
expect(rect3.boundElements).toEqual(null);
expect(arrow3.points).toCloselyEqualPoints([ expect(arrow3.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[45, 45], [0, 42.5],
[95, 95], [84, 42.5],
[84, 85],
]); ]);
}, },
); );

View file

@ -13,7 +13,7 @@ import {
isLinearElement, isLinearElement,
} from "@excalidraw/element/typeChecks"; } from "@excalidraw/element/typeChecks";
import { KEYS, arrayToMap, updateActiveTool } from "@excalidraw/common"; import { KEYS, updateActiveTool } from "@excalidraw/common";
import { isPathALoop } from "@excalidraw/element/shapes"; import { isPathALoop } from "@excalidraw/element/shapes";
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers"; import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
@ -153,15 +153,9 @@ export const actionFinalize = register({
!isLoop && !isLoop &&
multiPointElement.points.length > 1 multiPointElement.points.length > 1
) { ) {
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiPointElement,
-1,
arrayToMap(elements),
);
maybeBindLinearElement( maybeBindLinearElement(
multiPointElement, multiPointElement,
appState, appState,
{ x, y },
elementsMap, elementsMap,
elements, elements,
); );

View file

@ -2770,7 +2770,6 @@ class App extends React.Component<AppProps, AppState> {
this.updateEmbeddables(); this.updateEmbeddables();
const elements = this.scene.getElementsIncludingDeleted(); const elements = this.scene.getElementsIncludingDeleted();
const elementsMap = this.scene.getElementsMapIncludingDeleted(); const elementsMap = this.scene.getElementsMapIncludingDeleted();
const nonDeletedElementsMap = this.scene.getNonDeletedElementsMap();
if (!this.state.showWelcomeScreen && !elements.length) { if (!this.state.showWelcomeScreen && !elements.length) {
this.setState({ showWelcomeScreen: true }); this.setState({ showWelcomeScreen: true });
@ -2927,13 +2926,6 @@ class App extends React.Component<AppProps, AppState> {
maybeBindLinearElement( maybeBindLinearElement(
multiElement, multiElement,
this.state, this.state,
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiElement,
-1,
nonDeletedElementsMap,
),
),
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
); );
@ -9174,7 +9166,6 @@ class App extends React.Component<AppProps, AppState> {
maybeBindLinearElement( maybeBindLinearElement(
newElement, newElement,
this.state, this.state,
pointerCoords,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
); );