Simplified binding

This commit is contained in:
Mark Tolmacs
2025-10-31 16:37:14 +01:00
parent 5f108351a0
commit 2997594492
3 changed files with 174 additions and 12 deletions

View File

@@ -1,4 +1,10 @@
import { KEYS, arrayToMap, invariant, isTransparent } from "@excalidraw/common";
import {
KEYS,
arrayToMap,
getFeatureFlag,
invariant,
isTransparent,
} from "@excalidraw/common";
import {
lineSegment,
@@ -31,6 +37,7 @@ import {
getHoveredElementForBinding,
intersectElementWithLineSegment,
isBindableElementInsideOtherBindable,
isPointInElement,
} from "./collision";
import { distanceToElement } from "./distance";
import {
@@ -524,6 +531,140 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
shiftKey?: boolean;
finalize?: boolean;
},
): { start: BindingStrategy; end: BindingStrategy } => {
if (getFeatureFlag("COMPLEX_BINDINGS")) {
return getBindingStrategyForDraggingBindingElementEndpoints_complex(
arrow,
draggingPoints,
elementsMap,
elements,
appState,
opts,
);
}
return getBindingStrategyForDraggingBindingElementEndpoints_simple(
arrow,
draggingPoints,
elementsMap,
elements,
appState,
opts,
);
};
const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
appState: AppState,
opts?: {
newArrow?: boolean;
shiftKey?: boolean;
finalize?: boolean;
},
): { start: BindingStrategy; end: BindingStrategy } => {
const startIdx = 0;
const endIdx = arrow.points.length - 1;
const startDragged = draggingPoints.has(startIdx);
const endDragged = draggingPoints.has(endIdx);
let start: BindingStrategy = { mode: undefined };
let end: BindingStrategy = { mode: undefined };
invariant(
arrow.points.length > 1,
"Do not attempt to bind linear elements with a single point",
);
// If none of the ends are dragged, we don't change anything
if (!startDragged && !endDragged) {
return { start, end };
}
// If both ends are dragged, we don't bind to anything
// and break existing bindings
if (startDragged && endDragged) {
return { start: { mode: null }, end: { mode: null } };
}
// If binding is disabled and an endpoint is dragged,
// we actively break the end binding
if (!isBindingEnabled(appState)) {
start = startDragged ? { mode: null } : start;
end = endDragged ? { mode: null } : end;
return { start, end };
}
// Handle simpler elbow arrow binding
if (isElbowArrow(arrow)) {
return bindingStrategyForElbowArrowEndpointDragging(
arrow,
draggingPoints,
elementsMap,
elements,
);
}
const localPoint = draggingPoints.get(
startDragged ? startIdx : endIdx,
)?.point;
invariant(
localPoint,
`Local point must be defined for ${
startDragged ? "start" : "end"
} dragging`,
);
const globalPoint = LinearElementEditor.getPointGlobalCoordinates(
arrow,
localPoint,
elementsMap,
);
const hit = getHoveredElementForBinding(
globalPoint,
elements,
elementsMap,
(e) => 100, // TODO: Zoom-level
);
const current: BindingStrategy = hit
? isPointInElement(globalPoint, hit, elementsMap)
? {
mode: "inside",
element: hit,
focusPoint: globalPoint,
}
: {
mode: "orbit",
element: hit,
focusPoint: opts?.finalize
? LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrow,
startDragged ? 0 : -1,
elementsMap,
)
: globalPoint,
}
: { mode: null };
return {
start: startDragged ? current : start,
end: endDragged ? current : end,
};
};
const getBindingStrategyForDraggingBindingElementEndpoints_complex = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
appState: AppState,
opts?: {
newArrow?: boolean;
shiftKey?: boolean;
finalize?: boolean;
},
): { start: BindingStrategy; end: BindingStrategy } => {
const globalBindMode = appState.bindMode || "orbit";
const startIdx = 0;

View File

@@ -21,6 +21,7 @@ import {
getGridPoint,
invariant,
isShallowEqual,
getFeatureFlag,
} from "@excalidraw/common";
import {
@@ -2236,9 +2237,12 @@ const pointDraggingUpdates = (
nextArrow.endBinding.elementId,
)! as ExcalidrawBindableElement)
: null;
const endLocalPoint = startIsDraggingOverEndElement
? nextArrow.points[nextArrow.points.length - 1]
: endIsDraggingOverStartElement && app.state.bindMode !== "inside"
: endIsDraggingOverStartElement &&
app.state.bindMode !== "inside" &&
getFeatureFlag("COMPLEX_BINDINGS")
? nextArrow.points[0]
: endBindable
? updateBoundPoint(
@@ -2266,7 +2270,9 @@ const pointDraggingUpdates = (
const startLocalPoint = endIsDraggingOverStartElement
? nextArrow.points[0]
: startIsDraggingOverEndElement && app.state.bindMode !== "inside"
: startIsDraggingOverEndElement &&
app.state.bindMode !== "inside" &&
getFeatureFlag("COMPLEX_BINDINGS")
? nextArrow.points[nextArrow.points.length - 1]
: startBindable
? updateBoundPoint(

View File

@@ -106,6 +106,7 @@ import {
MQ_MAX_TABLET,
MQ_MAX_HEIGHT_LANDSCAPE,
MQ_MAX_WIDTH_LANDSCAPE,
getFeatureFlag,
} from "@excalidraw/common";
import {
@@ -4784,7 +4785,7 @@ class App extends React.Component<AppProps, AppState> {
}
// Handle Alt key for bind mode
if (event.key === KEYS.ALT) {
if (event.key === KEYS.ALT && getFeatureFlag("COMPLEX_BINDINGS")) {
this.handleSkipBindMode();
}
@@ -4797,7 +4798,10 @@ class App extends React.Component<AppProps, AppState> {
}
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
this.resetDelayedBindMode();
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.resetDelayedBindMode();
}
this.setState({ isBindingEnabled: false });
}
@@ -5103,7 +5107,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(),
);
if (isBindingElement(element)) {
if (isBindingElement(element) && getFeatureFlag("COMPLEX_BINDINGS")) {
this.handleDelayedBindModeChange(element, hoveredElement);
}
}
@@ -6542,7 +6546,9 @@ class App extends React.Component<AppProps, AppState> {
elementsMap,
);
this.handleDelayedBindModeChange(multiElement, hoveredElement);
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.handleDelayedBindModeChange(multiElement, hoveredElement);
}
}
invariant(
@@ -7348,7 +7354,10 @@ class App extends React.Component<AppProps, AppState> {
private handleCanvasPointerUp = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
this.resetDelayedBindMode();
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.resetDelayedBindMode();
}
this.removePointer(event);
this.lastPointerUpEvent = event;
@@ -8655,7 +8664,7 @@ class App extends React.Component<AppProps, AppState> {
});
});
if (isBindingElement(element)) {
if (isBindingElement(element) && getFeatureFlag("COMPLEX_BINDINGS")) {
this.handleDelayedBindModeChange(element, boundElement);
}
}
@@ -9023,12 +9032,16 @@ class App extends React.Component<AppProps, AppState> {
elementsMap,
);
this.handleDelayedBindModeChange(element, hoveredElement);
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.handleDelayedBindModeChange(element, hoveredElement);
}
}
if (
event.altKey &&
!this.state.selectedLinearElement?.initialState?.arrowStartIsInside
!this.state.selectedLinearElement?.initialState
?.arrowStartIsInside &&
getFeatureFlag("COMPLEX_BINDINGS")
) {
this.handleSkipBindMode();
}
@@ -9784,7 +9797,9 @@ class App extends React.Component<AppProps, AppState> {
});
}
this.resetDelayedBindMode();
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.resetDelayedBindMode();
}
this.setState({
selectedElementsAreBeingDragged: false,