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 { import {
getAllHoveredElementAtPoint, getAllHoveredElementAtPoint,
getHoveredElementForBinding, getHoveredElementForBinding,
hitElementItself,
intersectElementWithLineSegment, intersectElementWithLineSegment,
isPointInElement, isPointInElement,
} from "./collision"; } 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 = ( export const bindBindingElement = (
arrow: NonDeleted<ExcalidrawArrowElement>, arrow: NonDeleted<ExcalidrawArrowElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,

View File

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

View File

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