fix: Binding suggestions

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2025-09-07 18:22:33 +02:00
parent be56e84596
commit 433774e892
3 changed files with 30 additions and 106 deletions

View File

@@ -35,7 +35,6 @@ import {
import {
getAllHoveredElementAtPoint,
getHoveredElementForBinding,
hitElementItself,
intersectElementWithLineSegment,
isPointInElement,
} from "./collision";
@@ -644,68 +643,6 @@ export const bindOrUnbindBindingElements = (
});
};
export const maybeSuggestBindingsForBindingElementAtCoords = (
linearElement: NonDeleted<ExcalidrawArrowElement>,
startOrEndOrBoth: "start" | "end" | "both",
scene: Scene,
pointerCoords: GlobalPoint,
): AppState["suggestedBinding"] => {
const startCoords =
startOrEndOrBoth === "start"
? pointerCoords
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
0,
scene.getNonDeletedElementsMap(),
);
const endCoords =
startOrEndOrBoth === "end"
? pointerCoords
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
-1,
scene.getNonDeletedElementsMap(),
);
const startHovered = getHoveredElementForBinding(
startCoords,
scene.getNonDeletedElements(),
scene.getNonDeletedElementsMap(),
);
const endHovered = getHoveredElementForBinding(
endCoords,
scene.getNonDeletedElements(),
scene.getNonDeletedElementsMap(),
);
let suggestedBinding: AppState["suggestedBinding"] = null;
if (startHovered != null && startHovered.id === endHovered?.id) {
const hitStart = hitElementItself({
element: startHovered,
elementsMap: scene.getNonDeletedElementsMap(),
point: pointFrom<GlobalPoint>(startCoords[0], startCoords[1]),
threshold: 0,
overrideShouldTestInside: true,
});
const hitEnd = hitElementItself({
element: endHovered,
elementsMap: scene.getNonDeletedElementsMap(),
point: pointFrom<GlobalPoint>(endCoords[0], endCoords[1]),
threshold: 0,
overrideShouldTestInside: true,
});
if (hitStart && hitEnd) {
suggestedBinding = startHovered;
}
} else if (startOrEndOrBoth === "start" && startHovered != null) {
suggestedBinding = startHovered;
} else if (startOrEndOrBoth === "end" && endHovered != null) {
suggestedBinding = endHovered;
}
return suggestedBinding;
};
export const bindBindingElement = (
arrow: NonDeleted<ExcalidrawArrowElement>,
hoveredElement: ExcalidrawBindableElement,

View File

@@ -45,7 +45,6 @@ import {
calculateFixedPointForNonElbowArrowBinding,
getBindingStrategyForDraggingBindingElementEndpoints,
isBindingEnabled,
maybeSuggestBindingsForBindingElementAtCoords,
updateBoundPoint,
} from "./binding";
import {
@@ -335,6 +334,7 @@ export class LinearElementEditor {
}
// Apply the point movement if needed
let suggestedBinding: AppState["suggestedBinding"] = null;
if (deltaX || deltaY) {
const { positions, updates } = pointDraggingUpdates(
[idx],
@@ -347,6 +347,12 @@ export class LinearElementEditor {
);
LinearElementEditor.movePoints(element, app.scene, positions, updates);
// Set the suggested binding from the updates if available
if (isBindingElement(element, false)) {
if (isBindingEnabled(app.state)) {
suggestedBinding = updates?.suggestedBinding ?? null;
}
}
// Move the arrow over the bindable object in terms of z-index
if (isBindingElement(element)) {
@@ -364,17 +370,6 @@ export class LinearElementEditor {
}
}
// Suggest bindings for first and last point if selected
let suggestedBinding: AppState["suggestedBinding"] = null;
if (isBindingElement(element, false)) {
suggestedBinding = maybeSuggestBindingsForBindingElementAtCoords(
element,
"end",
app.scene,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
);
}
// PERF: Avoid state updates if not absolutely necessary
if (
app.state.selectedLinearElement?.customLineAngle === customLineAngle &&
@@ -530,22 +525,6 @@ export class LinearElementEditor {
handleBindTextResize(element, app.scene, false);
}
// Suggest bindings for first and last point if selected
if (isBindingElement(element, false)) {
if (isBindingEnabled(app.state) && (startIsSelected || endIsSelected)) {
suggestedBinding = maybeSuggestBindingsForBindingElementAtCoords(
element,
startIsSelected && endIsSelected
? "both"
: startIsSelected
? "start"
: "end",
app.scene,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
);
}
}
// Update selected points for elbow arrows because elbow arrows add and
// remove points as they route
const newSelectedPointsIndices = elbowed
@@ -2115,7 +2094,10 @@ const pointDraggingUpdates = (
if (startIsDragged) {
updates.suggestedBinding = start.element;
}
} else if (startIsDragged) {
updates.suggestedBinding = app.state.suggestedBinding;
}
if (end.mode === null) {
updates.endBinding = null;
} else if (end.mode) {
@@ -2134,6 +2116,8 @@ const pointDraggingUpdates = (
if (endIsDragged) {
updates.suggestedBinding = end.element;
}
} else if (endIsDragged) {
updates.suggestedBinding = app.state.suggestedBinding;
}
// Simulate the updated arrow for the bind point calculation
@@ -2258,11 +2242,13 @@ const pointDraggingUpdates = (
const indices = Array.from(indicesSet);
return {
updates: updates.startBinding
? {
startBinding: updates.startBinding,
}
: undefined,
updates:
updates.startBinding || updates.suggestedBinding
? {
startBinding: updates.startBinding,
suggestedBinding: updates.suggestedBinding,
}
: undefined,
positions: new Map(
indices.map((idx) => {
return [

View File

@@ -110,7 +110,6 @@ import {
import {
getObservedAppState,
getCommonBounds,
maybeSuggestBindingsForBindingElementAtCoords,
getElementAbsoluteCoords,
bindOrUnbindBindingElements,
fixBindingsAfterDeletion,
@@ -6246,15 +6245,17 @@ class App extends React.Component<AppProps, AppState> {
// Hovering with a selected tool or creating new linear element via click
// and point
const { newElement } = this.state;
if (isBindingElement(newElement, false) && isBindingEnabled(this.state)) {
this.setState({
suggestedBinding: maybeSuggestBindingsForBindingElementAtCoords(
newElement,
"end",
this.scene,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
),
});
if (!newElement && isBindingEnabled(this.state)) {
const hoveredElement = getHoveredElementForBinding(
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
);
if (hoveredElement) {
this.setState({
suggestedBinding: hoveredElement,
});
}
}
}