Refactor binding suggestions

This commit is contained in:
Mark Tolmacs 2025-05-16 16:46:56 +02:00
parent 95d89a751a
commit 9af3177dbd
No known key found for this signature in database
3 changed files with 94 additions and 83 deletions

View File

@ -398,6 +398,51 @@ export const getSuggestedBindingsForArrows = (
); );
}; };
export const maybeSuggestBindingsForLinearElementAtCoords = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
/** scene coords */
pointerCoords: {
x: number;
y: number;
}[],
scene: Scene,
zoom: AppState["zoom"],
// During line creation the start binding hasn't been written yet
// into `linearElement`
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
): ExcalidrawBindableElement[] => {
if (!pointerCoords.length) {
return [];
}
const suggestedBindings = pointerCoords.reduce(
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding(
coords,
scene.getNonDeletedElements(),
scene.getNonDeletedElementsMap(),
zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
);
if (
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
) {
acc.push(hoveredBindableElement);
}
return acc;
},
[],
);
return suggestedBindings;
};
export const maybeBindLinearElement = ( export const maybeBindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
appState: AppState, appState: AppState,

View File

@ -39,6 +39,7 @@ import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
getHoveredElementForBinding, getHoveredElementForBinding,
isBindingEnabled, isBindingEnabled,
maybeSuggestBindingsForLinearElementAtCoords,
} from "./binding"; } from "./binding";
import { import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
@ -245,18 +246,13 @@ export class LinearElementEditor {
app: AppClassProperties, app: AppClassProperties,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[],
) => void,
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
scene: Scene, ): Pick<AppState, keyof AppState> | null {
): LinearElementEditor | null {
if (!linearElementEditor) { if (!linearElementEditor) {
return null; return null;
} }
const { elementId } = linearElementEditor; const { elementId } = linearElementEditor;
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return null; return null;
@ -309,7 +305,7 @@ export class LinearElementEditor {
LinearElementEditor.movePoints( LinearElementEditor.movePoints(
element, element,
scene, app.scene,
new Map([ new Map([
[ [
selectedIndex, selectedIndex,
@ -337,7 +333,7 @@ export class LinearElementEditor {
LinearElementEditor.movePoints( LinearElementEditor.movePoints(
element, element,
scene, app.scene,
new Map( new Map(
selectedPointsIndices.map((pointIndex) => { selectedPointsIndices.map((pointIndex) => {
const newPointPosition: LocalPoint = const newPointPosition: LocalPoint =
@ -369,10 +365,11 @@ export class LinearElementEditor {
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) { if (boundTextElement) {
handleBindTextResize(element, scene, false); handleBindTextResize(element, app.scene, false);
} }
// suggest bindings for first and last point if selected // suggest bindings for first and last point if selected
let suggestedBindings: ExcalidrawBindableElement[] = [];
if (isBindingElement(element, false)) { if (isBindingElement(element, false)) {
const coords: { x: number; y: number }[] = []; const coords: { x: number; y: number }[] = [];
@ -404,11 +401,16 @@ export class LinearElementEditor {
} }
if (coords.length) { if (coords.length) {
maybeSuggestBinding(element, coords); suggestedBindings = maybeSuggestBindingsForLinearElementAtCoords(
element,
coords,
app.scene,
app.state.zoom,
);
} }
} }
return { const newLinearElementEditor = {
...linearElementEditor, ...linearElementEditor,
selectedPointsIndices, selectedPointsIndices,
segmentMidPointHoveredCoords: segmentMidPointHoveredCoords:
@ -427,6 +429,15 @@ export class LinearElementEditor {
: -1, : -1,
isDragging: true, isDragging: true,
}; };
return {
...app.state,
editingLinearElement: app.state.editingLinearElement
? newLinearElementEditor
: null,
selectedLinearElement: newLinearElementEditor,
suggestedBindings,
};
} }
return null; return null;

View File

@ -104,7 +104,11 @@ import {
Emitter, Emitter,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element"; import {
getCommonBounds,
getElementAbsoluteCoords,
maybeSuggestBindingsForLinearElementAtCoords,
} from "@excalidraw/element";
import { import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
@ -112,7 +116,6 @@ import {
fixBindingsAfterDeletion, fixBindingsAfterDeletion,
getHoveredElementForBinding, getHoveredElementForBinding,
isBindingEnabled, isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement, maybeBindLinearElement,
shouldEnableBindingForPointerEvent, shouldEnableBindingForPointerEvent,
updateBoundElements, updateBoundElements,
@ -299,7 +302,6 @@ import type { ElementUpdate } from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math"; import type { LocalPoint, Radians } from "@excalidraw/math";
import type { import type {
ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
ExcalidrawGenericElement, ExcalidrawGenericElement,
@ -5961,11 +5963,15 @@ class App extends React.Component<AppProps, AppState> {
// and point // and point
const { newElement } = this.state; const { newElement } = this.state;
if (isBindingElement(newElement, false)) { if (isBindingElement(newElement, false)) {
this.maybeSuggestBindingsForLinearElementAtCoords( this.setState({
newElement, suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords(
[scenePointer], newElement,
this.state.startBoundElement, [scenePointer],
); this.scene,
this.state.zoom,
this.state.startBoundElement,
),
});
} else { } else {
this.maybeSuggestBindingAtCursor(scenePointer, false); this.maybeSuggestBindingAtCursor(scenePointer, false);
} }
@ -8251,31 +8257,19 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const newLinearElementEditor = LinearElementEditor.handlePointDragging( const newState = LinearElementEditor.handlePointDragging(
event, event,
this, this,
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
(element, pointsSceneCoords) => {
this.maybeSuggestBindingsForLinearElementAtCoords(
element,
pointsSceneCoords,
);
},
linearElementEditor, linearElementEditor,
this.scene,
); );
if (newLinearElementEditor) { if (newState) {
pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y; pointerDownState.lastCoords.y = pointerCoords.y;
pointerDownState.drag.hasOccurred = true; pointerDownState.drag.hasOccurred = true;
this.setState({ this.setState(newState);
editingLinearElement: this.state.editingLinearElement
? newLinearElementEditor
: null,
selectedLinearElement: newLinearElementEditor,
});
return; return;
} }
@ -8754,11 +8748,15 @@ class App extends React.Component<AppProps, AppState> {
if (isBindingElement(newElement, false)) { if (isBindingElement(newElement, false)) {
// When creating a linear element by dragging // When creating a linear element by dragging
this.maybeSuggestBindingsForLinearElementAtCoords( this.setState({
newElement, suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords(
[pointerCoords], newElement,
this.state.startBoundElement, [pointerCoords],
); this.scene,
this.state.zoom,
this.state.startBoundElement,
),
});
} }
} else { } else {
pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.x = pointerCoords.x;
@ -10285,49 +10283,6 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
private maybeSuggestBindingsForLinearElementAtCoords = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
/** scene coords */
pointerCoords: {
x: number;
y: number;
}[],
// During line creation the start binding hasn't been written yet
// into `linearElement`
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
): void => {
if (!pointerCoords.length) {
return;
}
const suggestedBindings = pointerCoords.reduce(
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding(
coords,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
);
if (
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
) {
acc.push(hoveredBindableElement);
}
return acc;
},
[],
);
this.setState({ suggestedBindings });
};
private clearSelection(hitElement: ExcalidrawElement | null): void { private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({ this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState), selectedElementIds: makeNextSelectedElementIds({}, prevState),