|
|
@ -25,7 +25,7 @@ import type {
|
|
|
|
} from "./types";
|
|
|
|
} from "./types";
|
|
|
|
|
|
|
|
|
|
|
|
import { getElementAbsoluteCoords } from "./bounds";
|
|
|
|
import { getElementAbsoluteCoords } from "./bounds";
|
|
|
|
import type { AppClassProperties, AppState, Point } from "../types";
|
|
|
|
import type { AppState, Point } from "../types";
|
|
|
|
import { isPointOnShape } from "../../utils/collision";
|
|
|
|
import { isPointOnShape } from "../../utils/collision";
|
|
|
|
import { getElementAtPosition } from "../scene";
|
|
|
|
import { getElementAtPosition } from "../scene";
|
|
|
|
import {
|
|
|
|
import {
|
|
|
@ -43,6 +43,7 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|
|
|
import { arrayToMap, tupleToCoors } from "../utils";
|
|
|
|
import { arrayToMap, tupleToCoors } from "../utils";
|
|
|
|
import { KEYS } from "../keys";
|
|
|
|
import { KEYS } from "../keys";
|
|
|
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
|
|
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
|
|
|
|
|
|
|
import { getElementShape } from "../shapes";
|
|
|
|
|
|
|
|
|
|
|
|
export type SuggestedBinding =
|
|
|
|
export type SuggestedBinding =
|
|
|
|
| NonDeleted<ExcalidrawBindableElement>
|
|
|
|
| NonDeleted<ExcalidrawBindableElement>
|
|
|
@ -179,9 +180,8 @@ const bindOrUnbindLinearElementEdge = (
|
|
|
|
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|
|
|
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
edge: "start" | "end",
|
|
|
|
edge: "start" | "end",
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
): NonDeleted<ExcalidrawElement> | null => {
|
|
|
|
): NonDeleted<ExcalidrawElement> | null => {
|
|
|
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
|
|
|
|
|
|
|
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
|
|
|
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
|
|
|
const elementId =
|
|
|
|
const elementId =
|
|
|
|
edge === "start"
|
|
|
|
edge === "start"
|
|
|
@ -189,7 +189,10 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|
|
|
: linearElement.endBinding?.elementId;
|
|
|
|
: linearElement.endBinding?.elementId;
|
|
|
|
if (elementId) {
|
|
|
|
if (elementId) {
|
|
|
|
const element = elementsMap.get(elementId);
|
|
|
|
const element = elementsMap.get(elementId);
|
|
|
|
if (isBindableElement(element) && bindingBorderTest(element, coors, app)) {
|
|
|
|
if (
|
|
|
|
|
|
|
|
isBindableElement(element) &&
|
|
|
|
|
|
|
|
bindingBorderTest(element, coors, elementsMap)
|
|
|
|
|
|
|
|
) {
|
|
|
|
return element;
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -199,13 +202,13 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|
|
|
|
|
|
|
|
|
|
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
|
|
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
): (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",
|
|
|
|
app,
|
|
|
|
elementsMap,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
@ -213,7 +216,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|
|
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
isBindingEnabled: boolean,
|
|
|
|
isBindingEnabled: boolean,
|
|
|
|
draggingPoints: readonly number[],
|
|
|
|
draggingPoints: readonly number[],
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
): (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;
|
|
|
@ -221,37 +224,57 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|
|
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
|
|
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
|
|
|
const start = startDragged
|
|
|
|
const start = startDragged
|
|
|
|
? isBindingEnabled
|
|
|
|
? isBindingEnabled
|
|
|
|
? getElligibleElementForBindingElement(selectedElement, "start", app)
|
|
|
|
? getElligibleElementForBindingElement(
|
|
|
|
|
|
|
|
selectedElement,
|
|
|
|
|
|
|
|
"start",
|
|
|
|
|
|
|
|
elementsMap,
|
|
|
|
|
|
|
|
)
|
|
|
|
: 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
|
|
|
|
getElligibleElementForBindingElement(selectedElement, "start", app);
|
|
|
|
getElligibleElementForBindingElement(
|
|
|
|
|
|
|
|
selectedElement,
|
|
|
|
|
|
|
|
"start",
|
|
|
|
|
|
|
|
elementsMap,
|
|
|
|
|
|
|
|
);
|
|
|
|
const end = endDragged
|
|
|
|
const end = endDragged
|
|
|
|
? isBindingEnabled
|
|
|
|
? isBindingEnabled
|
|
|
|
? getElligibleElementForBindingElement(selectedElement, "end", app)
|
|
|
|
? getElligibleElementForBindingElement(
|
|
|
|
|
|
|
|
selectedElement,
|
|
|
|
|
|
|
|
"end",
|
|
|
|
|
|
|
|
elementsMap,
|
|
|
|
|
|
|
|
)
|
|
|
|
: 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
|
|
|
|
getElligibleElementForBindingElement(selectedElement, "end", app);
|
|
|
|
getElligibleElementForBindingElement(selectedElement, "end", elementsMap);
|
|
|
|
|
|
|
|
|
|
|
|
return [start, end];
|
|
|
|
return [start, end];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getBindingStrategyForDraggingArrowOrJoints = (
|
|
|
|
const getBindingStrategyForDraggingArrowOrJoints = (
|
|
|
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
isBindingEnabled: boolean,
|
|
|
|
isBindingEnabled: boolean,
|
|
|
|
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
|
|
|
): (NonDeleted<ExcalidrawBindableElement> | null | "keep")[] => {
|
|
|
|
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
|
|
|
|
const [startIsClose, endIsClose] = getOriginalBindingsIfStillCloseToArrowEnds(
|
|
|
|
selectedElement,
|
|
|
|
selectedElement,
|
|
|
|
app,
|
|
|
|
elementsMap,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
const start = startIsClose
|
|
|
|
const start = startIsClose
|
|
|
|
? isBindingEnabled
|
|
|
|
? isBindingEnabled
|
|
|
|
? getElligibleElementForBindingElement(selectedElement, "start", app)
|
|
|
|
? getElligibleElementForBindingElement(
|
|
|
|
|
|
|
|
selectedElement,
|
|
|
|
|
|
|
|
"start",
|
|
|
|
|
|
|
|
elementsMap,
|
|
|
|
|
|
|
|
)
|
|
|
|
: null
|
|
|
|
: null
|
|
|
|
: null;
|
|
|
|
: null;
|
|
|
|
const end = endIsClose
|
|
|
|
const end = endIsClose
|
|
|
|
? isBindingEnabled
|
|
|
|
? isBindingEnabled
|
|
|
|
? getElligibleElementForBindingElement(selectedElement, "end", app)
|
|
|
|
? getElligibleElementForBindingElement(
|
|
|
|
|
|
|
|
selectedElement,
|
|
|
|
|
|
|
|
"end",
|
|
|
|
|
|
|
|
elementsMap,
|
|
|
|
|
|
|
|
)
|
|
|
|
: null
|
|
|
|
: null
|
|
|
|
: null;
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
@ -260,7 +283,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|
|
|
|
|
|
|
|
|
|
|
export const bindOrUnbindLinearElements = (
|
|
|
|
export const bindOrUnbindLinearElements = (
|
|
|
|
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
|
|
|
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
isBindingEnabled: boolean,
|
|
|
|
isBindingEnabled: boolean,
|
|
|
|
draggingPoints: readonly number[] | null,
|
|
|
|
draggingPoints: readonly number[] | null,
|
|
|
|
): void => {
|
|
|
|
): void => {
|
|
|
@ -271,27 +294,22 @@ export const bindOrUnbindLinearElements = (
|
|
|
|
selectedElement,
|
|
|
|
selectedElement,
|
|
|
|
isBindingEnabled,
|
|
|
|
isBindingEnabled,
|
|
|
|
draggingPoints ?? [],
|
|
|
|
draggingPoints ?? [],
|
|
|
|
app,
|
|
|
|
elementsMap,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
: // The arrow itself (the shaft) or the inner joins are dragged
|
|
|
|
: // The arrow itself (the shaft) or the inner joins are dragged
|
|
|
|
getBindingStrategyForDraggingArrowOrJoints(
|
|
|
|
getBindingStrategyForDraggingArrowOrJoints(
|
|
|
|
selectedElement,
|
|
|
|
selectedElement,
|
|
|
|
app,
|
|
|
|
elementsMap,
|
|
|
|
isBindingEnabled,
|
|
|
|
isBindingEnabled,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
bindOrUnbindLinearElement(
|
|
|
|
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap);
|
|
|
|
selectedElement,
|
|
|
|
|
|
|
|
start,
|
|
|
|
|
|
|
|
end,
|
|
|
|
|
|
|
|
app.scene.getNonDeletedElementsMap(),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const getSuggestedBindingsForArrows = (
|
|
|
|
export const getSuggestedBindingsForArrows = (
|
|
|
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
|
|
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
): 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) {
|
|
|
@ -302,7 +320,7 @@ export const getSuggestedBindingsForArrows = (
|
|
|
|
selectedElements
|
|
|
|
selectedElements
|
|
|
|
.filter(isLinearElement)
|
|
|
|
.filter(isLinearElement)
|
|
|
|
.flatMap((element) =>
|
|
|
|
.flatMap((element) =>
|
|
|
|
getOriginalBindingsIfStillCloseToArrowEnds(element, app),
|
|
|
|
getOriginalBindingsIfStillCloseToArrowEnds(element, elementsMap),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.filter(
|
|
|
|
.filter(
|
|
|
|
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
|
|
|
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
|
|
@ -324,17 +342,20 @@ export const maybeBindLinearElement = (
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
appState: AppState,
|
|
|
|
appState: AppState,
|
|
|
|
pointerCoords: { x: number; y: number },
|
|
|
|
pointerCoords: { x: number; y: number },
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
): void => {
|
|
|
|
): void => {
|
|
|
|
if (appState.startBoundElement != null) {
|
|
|
|
if (appState.startBoundElement != null) {
|
|
|
|
bindLinearElement(
|
|
|
|
bindLinearElement(
|
|
|
|
linearElement,
|
|
|
|
linearElement,
|
|
|
|
appState.startBoundElement,
|
|
|
|
appState.startBoundElement,
|
|
|
|
"start",
|
|
|
|
"start",
|
|
|
|
app.scene.getNonDeletedElementsMap(),
|
|
|
|
elementsMap,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const hoveredElement = getHoveredElementForBinding(pointerCoords, app);
|
|
|
|
const hoveredElement = getHoveredElementForBinding(
|
|
|
|
|
|
|
|
pointerCoords,
|
|
|
|
|
|
|
|
elementsMap,
|
|
|
|
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
if (
|
|
|
|
hoveredElement != null &&
|
|
|
|
hoveredElement != null &&
|
|
|
|
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
|
|
|
|
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
|
|
|
@ -343,12 +364,7 @@ export const maybeBindLinearElement = (
|
|
|
|
"end",
|
|
|
|
"end",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
bindLinearElement(
|
|
|
|
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
|
|
|
linearElement,
|
|
|
|
|
|
|
|
hoveredElement,
|
|
|
|
|
|
|
|
"end",
|
|
|
|
|
|
|
|
app.scene.getNonDeletedElementsMap(),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
@ -432,13 +448,13 @@ export const getHoveredElementForBinding = (
|
|
|
|
x: number;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
y: number;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
|
|
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
|
|
|
const hoveredElement = getElementAtPosition(
|
|
|
|
const hoveredElement = getElementAtPosition(
|
|
|
|
app.scene.getNonDeletedElements(),
|
|
|
|
[...elementsMap].map(([_, value]) => value),
|
|
|
|
(element) =>
|
|
|
|
(element) =>
|
|
|
|
isBindableElement(element, false) &&
|
|
|
|
isBindableElement(element, false) &&
|
|
|
|
bindingBorderTest(element, pointerCoords, app),
|
|
|
|
bindingBorderTest(element, pointerCoords, elementsMap),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
|
|
|
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -662,15 +678,11 @@ const maybeCalculateNewGapWhenScaling = (
|
|
|
|
const getElligibleElementForBindingElement = (
|
|
|
|
const getElligibleElementForBindingElement = (
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
startOrEnd: "start" | "end",
|
|
|
|
startOrEnd: "start" | "end",
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
|
|
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
|
|
|
return getHoveredElementForBinding(
|
|
|
|
return getHoveredElementForBinding(
|
|
|
|
getLinearElementEdgeCoors(
|
|
|
|
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
|
|
|
|
linearElement,
|
|
|
|
elementsMap,
|
|
|
|
startOrEnd,
|
|
|
|
|
|
|
|
app.scene.getNonDeletedElementsMap(),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
app,
|
|
|
|
|
|
|
|
);
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
@ -834,10 +846,10 @@ const newBoundElements = (
|
|
|
|
const bindingBorderTest = (
|
|
|
|
const bindingBorderTest = (
|
|
|
|
element: NonDeleted<ExcalidrawBindableElement>,
|
|
|
|
element: NonDeleted<ExcalidrawBindableElement>,
|
|
|
|
{ x, y }: { x: number; y: number },
|
|
|
|
{ x, y }: { x: number; y: number },
|
|
|
|
app: AppClassProperties,
|
|
|
|
elementsMap: ElementsMap,
|
|
|
|
): boolean => {
|
|
|
|
): boolean => {
|
|
|
|
const threshold = maxBindingGap(element, element.width, element.height);
|
|
|
|
const threshold = maxBindingGap(element, element.width, element.height);
|
|
|
|
const shape = app.getElementShape(element);
|
|
|
|
const shape = getElementShape(element, elementsMap);
|
|
|
|
return isPointOnShape([x, y], shape, threshold);
|
|
|
|
return isPointOnShape([x, y], shape, threshold);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|