Compare commits

...

6 Commits

Author SHA1 Message Date
Márk Tolmács
d5931abbe2
Merge branch 'master' into mtolmacs/fix/arrow-snap-overlap 2025-05-20 09:26:40 +02:00
Mark Tolmacs
09479a0579
Fix multi-point drags 2025-05-17 15:31:50 +02:00
Mark Tolmacs
0a21aa083c
Snapshot update 2025-05-17 15:06:46 +02:00
Mark Tolmacs
cf9bfb5ebd
Fix binding 2025-05-16 17:45:08 +02:00
Mark Tolmacs
6deb4fdbe6
Fix the highlight part of the issue 2025-05-16 17:01:31 +02:00
Mark Tolmacs
9af3177dbd
Refactor binding suggestions 2025-05-16 16:46:56 +02:00
4 changed files with 136 additions and 116 deletions

View File

@ -398,6 +398,48 @@ 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[] =>
Array.from(
pointerCoords.reduce(
(acc: Set<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.add(hoveredBindableElement);
}
return acc;
},
new Set() as Set<NonDeleted<ExcalidrawBindableElement>>,
),
);
export const maybeBindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
appState: AppState,
@ -535,7 +577,7 @@ export const isLinearElementSimpleAndAlreadyBound = (
const isLinearElementSimple = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
): boolean => linearElement.points.length < 3;
): boolean => linearElement.points.length < 3 && !isElbowArrow(linearElement);
const unbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>,

View File

@ -18,6 +18,7 @@ import {
getGridPoint,
invariant,
tupleToCoors,
viewportCoordsToSceneCoords,
} from "@excalidraw/common";
import type { Store } from "@excalidraw/element";
@ -39,6 +40,7 @@ import {
bindOrUnbindLinearElement,
getHoveredElementForBinding,
isBindingEnabled,
maybeSuggestBindingsForLinearElementAtCoords,
} from "./binding";
import {
getElementAbsoluteCoords,
@ -240,18 +242,13 @@ export class LinearElementEditor {
app: AppClassProperties,
scenePointerX: number,
scenePointerY: number,
maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[],
) => void,
linearElementEditor: LinearElementEditor,
scene: Scene,
): LinearElementEditor | null {
): Pick<AppState, keyof AppState> | null {
if (!linearElementEditor) {
return null;
}
const { elementId } = linearElementEditor;
const elementsMap = scene.getNonDeletedElementsMap();
const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
return null;
@ -304,7 +301,7 @@ export class LinearElementEditor {
LinearElementEditor.movePoints(
element,
scene,
app.scene,
new Map([
[
selectedIndex,
@ -332,7 +329,7 @@ export class LinearElementEditor {
LinearElementEditor.movePoints(
element,
scene,
app.scene,
new Map(
selectedPointsIndices.map((pointIndex) => {
const newPointPosition: LocalPoint =
@ -364,46 +361,59 @@ export class LinearElementEditor {
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
handleBindTextResize(element, scene, false);
handleBindTextResize(element, app.scene, false);
}
// suggest bindings for first and last point if selected
let suggestedBindings: ExcalidrawBindableElement[] = [];
if (isBindingElement(element, false)) {
const firstSelectedIndex = selectedPointsIndices[0] === 0;
const lastSelectedIndex =
selectedPointsIndices[selectedPointsIndices.length - 1] ===
element.points.length - 1;
const coords: { x: number; y: number }[] = [];
const firstSelectedIndex = selectedPointsIndices[0];
if (firstSelectedIndex === 0) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
elementsMap,
if (!firstSelectedIndex !== !lastSelectedIndex) {
coords.push({ x: scenePointerX, y: scenePointerY });
} else {
if (firstSelectedIndex) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
elementsMap,
),
),
),
);
}
);
}
const lastSelectedIndex =
selectedPointsIndices[selectedPointsIndices.length - 1];
if (lastSelectedIndex === element.points.length - 1) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[lastSelectedIndex],
elementsMap,
if (lastSelectedIndex) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[
selectedPointsIndices[selectedPointsIndices.length - 1]
],
elementsMap,
),
),
),
);
);
}
}
if (coords.length) {
maybeSuggestBinding(element, coords);
suggestedBindings = maybeSuggestBindingsForLinearElementAtCoords(
element,
coords,
app.scene,
app.state.zoom,
);
}
}
return {
const newLinearElementEditor = {
...linearElementEditor,
selectedPointsIndices,
segmentMidPointHoveredCoords:
@ -422,6 +432,15 @@ export class LinearElementEditor {
: -1,
isDragging: true,
};
return {
...app.state,
editingLinearElement: app.state.editingLinearElement
? newLinearElementEditor
: null,
selectedLinearElement: newLinearElementEditor,
suggestedBindings,
};
}
return null;
@ -435,6 +454,7 @@ export class LinearElementEditor {
): LinearElementEditor {
const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
const pointerCoords = viewportCoordsToSceneCoords(event, appState);
const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
editingLinearElement;
@ -478,13 +498,15 @@ export class LinearElementEditor {
const bindingElement = isBindingEnabled(appState)
? getHoveredElementForBinding(
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
elementsMap,
),
),
(selectedPointsIndices?.length ?? 0) > 1
? tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
elementsMap,
),
)
: pointerCoords,
elements,
elementsMap,
appState.zoom,

View File

@ -104,7 +104,11 @@ import {
Emitter,
} from "@excalidraw/common";
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
import {
getCommonBounds,
getElementAbsoluteCoords,
maybeSuggestBindingsForLinearElementAtCoords,
} from "@excalidraw/element";
import {
bindOrUnbindLinearElement,
@ -112,7 +116,6 @@ import {
fixBindingsAfterDeletion,
getHoveredElementForBinding,
isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement,
shouldEnableBindingForPointerEvent,
updateBoundElements,
@ -299,7 +302,6 @@ import type { ElementUpdate } from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math";
import type {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFreeDrawElement,
ExcalidrawGenericElement,
@ -5961,11 +5963,15 @@ class App extends React.Component<AppProps, AppState> {
// and point
const { newElement } = this.state;
if (isBindingElement(newElement, false)) {
this.maybeSuggestBindingsForLinearElementAtCoords(
newElement,
[scenePointer],
this.state.startBoundElement,
);
this.setState({
suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords(
newElement,
[scenePointer],
this.scene,
this.state.zoom,
this.state.startBoundElement,
),
});
} else {
this.maybeSuggestBindingAtCursor(scenePointer, false);
}
@ -8251,31 +8257,19 @@ class App extends React.Component<AppProps, AppState> {
return;
}
const newLinearElementEditor = LinearElementEditor.handlePointDragging(
const newState = LinearElementEditor.handlePointDragging(
event,
this,
pointerCoords.x,
pointerCoords.y,
(element, pointsSceneCoords) => {
this.maybeSuggestBindingsForLinearElementAtCoords(
element,
pointsSceneCoords,
);
},
linearElementEditor,
this.scene,
);
if (newLinearElementEditor) {
if (newState) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
pointerDownState.drag.hasOccurred = true;
this.setState({
editingLinearElement: this.state.editingLinearElement
? newLinearElementEditor
: null,
selectedLinearElement: newLinearElementEditor,
});
this.setState(newState);
return;
}
@ -8754,11 +8748,15 @@ class App extends React.Component<AppProps, AppState> {
if (isBindingElement(newElement, false)) {
// When creating a linear element by dragging
this.maybeSuggestBindingsForLinearElementAtCoords(
newElement,
[pointerCoords],
this.state.startBoundElement,
);
this.setState({
suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords(
newElement,
[pointerCoords],
this.scene,
this.state.zoom,
this.state.startBoundElement,
),
});
}
} else {
pointerDownState.lastCoords.x = pointerCoords.x;
@ -9757,7 +9755,8 @@ class App extends React.Component<AppProps, AppState> {
}
if (
pointerDownState.drag.hasOccurred ||
(pointerDownState.drag.hasOccurred &&
!this.state.selectedLinearElement) ||
isResizing ||
isRotating ||
isCropping
@ -10285,49 +10284,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 {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),

View File

@ -227,7 +227,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 37,
"version": 35,
"width": "102.80179",
"x": "-0.42182",
"y": 0,
@ -808,7 +808,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 33,
"version": 31,
"width": 100,
"x": "149.29289",
"y": 0,