From 0e197ef5c4c1933d9651cf1c08d6a5c819de5cc5 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Thu, 26 Jun 2025 17:22:42 +1000 Subject: [PATCH] fix: do not snap to each other when moving multiple points together --- packages/element/src/linearElementEditor.ts | 17 ++--- packages/element/src/snapping.ts | 81 ++++++++------------- packages/excalidraw/components/App.tsx | 4 +- 3 files changed, 37 insertions(+), 65 deletions(-) diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index c28255c118..dc869a4dc6 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -370,17 +370,12 @@ export class LinearElementEditor { const effectiveGridX = referencePointCoords[0] + dxFromReference; const effectiveGridY = referencePointCoords[1] + dyFromReference; - let newDraggingPointPosition = pointFrom( - effectiveGridX, - effectiveGridY, - ); - if (!isElbowArrow(element)) { const { snapOffset, snapLines } = snapLinearElementPoint( app.scene.getNonDeletedElements(), element, lastClickedPoint, - { x: effectiveGridX, y: effectiveGridY }, + pointFrom(effectiveGridX, effectiveGridY), app, event, elementsMap, @@ -448,7 +443,7 @@ export class LinearElementEditor { -element.angle as Radians, ); - newDraggingPointPosition = pointFrom( + const newDraggingPointPosition = pointFrom( referencePoint[0] + rotatedX, referencePoint[1] + rotatedY, ); @@ -477,11 +472,11 @@ export class LinearElementEditor { app.scene.getNonDeletedElements(), element, lastClickedPoint, - { x: originalPointerX, y: originalPointerY }, + pointFrom(originalPointerX, originalPointerY), app, event, elementsMap, - { includeSelfPoints: true }, + { includeSelfPoints: true, selectedPointsIndices }, ); _snapLines = snapLines; @@ -1223,7 +1218,7 @@ export class LinearElementEditor { app.scene.getNonDeletedElements(), element, points.length - 1, - { x: effectiveGridX, y: effectiveGridY }, + pointFrom(effectiveGridX, effectiveGridY), app, event, elementsMap, @@ -1311,7 +1306,7 @@ export class LinearElementEditor { app.scene.getNonDeletedElements(), element, points.length - 1, - { x: originalPointerX, y: originalPointerY }, + pointFrom(originalPointerX, originalPointerY), app, event, elementsMap, diff --git a/packages/element/src/snapping.ts b/packages/element/src/snapping.ts index 424c02564c..5c4e217416 100644 --- a/packages/element/src/snapping.ts +++ b/packages/element/src/snapping.ts @@ -2,7 +2,6 @@ import { isCloseTo, pointFrom, pointRotateRads, - pointsEqual, rangeInclusive, rangeIntersection, rangesOverlap, @@ -198,13 +197,12 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => { export const getLinearElementPoints = ( element: ExcalidrawLinearElement, - elementsMap: ElementsMap, options: { dragOffset?: Vector2D; - excludePointIndex?: number; + excludePointsIndices?: readonly number[]; } = {}, ): GlobalPoint[] => { - const { dragOffset, excludePointIndex } = options; + const { dragOffset, excludePointsIndices } = options; if (isElbowArrow(element)) { return []; @@ -226,27 +224,25 @@ export const getLinearElementPoints = ( for (let i = 0; i < element.points.length; i++) { // Skip the point being edited if specified - if (excludePointIndex !== undefined && i === excludePointIndex) { + if ( + excludePointsIndices?.length && + excludePointsIndices.find((index) => index === i) !== undefined + ) { continue; } - const localPoint = element.points[i]; - const globalX = elementX + localPoint[0]; - const globalY = elementY + localPoint[1]; + const point = element.points[i]; + const globalX = elementX + point[0]; + const globalY = elementY + point[1]; - // Apply rotation if element is rotated - if (element.angle !== 0) { - const cx = elementX + element.width / 2; - const cy = elementY + element.height / 2; - const rotated = pointRotateRads( - pointFrom(globalX, globalY), - pointFrom(cx, cy), - element.angle, - ); - globalPoints.push(pointFrom(round(rotated[0]), round(rotated[1]))); - } else { - globalPoints.push(pointFrom(round(globalX), round(globalY))); - } + const cx = elementX + element.width / 2; + const cy = elementY + element.height / 2; + const rotated = pointRotateRads( + pointFrom(globalX, globalY), + pointFrom(cx, cy), + element.angle, + ); + globalPoints.push(pointFrom(round(rotated[0]), round(rotated[1]))); } return globalPoints; @@ -296,7 +292,7 @@ export const getElementsCorners = ( !boundingBoxCorners ) { // For linear elements, use actual points instead of bounding box - const linearPoints = getLinearElementPoints(element, elementsMap, { + const linearPoints = getLinearElementPoints(element, { dragOffset, }); result = linearPoints; @@ -714,6 +710,7 @@ export const getReferenceSnapPointsForLinearElementPoint = ( elementsMap: ElementsMap, options: { includeSelfPoints?: boolean; + selectedPointsIndices?: readonly number[]; } = {}, ) => { const { includeSelfPoints = false } = options; @@ -743,24 +740,9 @@ export const getReferenceSnapPointsForLinearElementPoint = ( // Include other points from the same linear element when creating new points or in editing mode if (includeSelfPoints) { - const elementPoints = getLinearElementPoints(editingElement, elementsMap, { - excludePointIndex: editingPointIndex >= 0 ? editingPointIndex : undefined, + const elementPoints = getLinearElementPoints(editingElement, { + excludePointsIndices: options.selectedPointsIndices, }); - const shouldSkipFirstOrLast = - editingElement.points.length > 2 && - pointsEqual( - editingElement.points[0], - editingElement.points[editingElement.points.length - 1], - ); - - if (shouldSkipFirstOrLast) { - if (editingPointIndex === 0) { - elementPoints.pop(); - } - if (editingPointIndex === editingElement.points.length - 1) { - elementPoints.shift(); - } - } allSnapPoints.push(...elementPoints); } @@ -771,12 +753,13 @@ export const snapLinearElementPoint = ( elements: readonly NonDeletedExcalidrawElement[], editingElement: ExcalidrawLinearElement, editingPointIndex: number, - pointPosition: Vector2D, + pointerPosition: GlobalPoint, app: AppClassProperties, event: KeyboardModifiersObject, elementsMap: ElementsMap, options: { includeSelfPoints?: boolean; + selectedPointsIndices?: readonly number[]; } = {}, ) => { if ( @@ -808,16 +791,10 @@ export const snapLinearElementPoint = ( options, ); - // Create a snap point for the current point position - const currentPointGlobal = pointFrom( - pointPosition.x, - pointPosition.y, - ); - // Find nearest snaps for (const referencePoint of referenceSnapPoints) { - const offsetX = referencePoint[0] - currentPointGlobal[0]; - const offsetY = referencePoint[1] - currentPointGlobal[1]; + const offsetX = referencePoint[0] - pointerPosition[0]; + const offsetY = referencePoint[1] - pointerPosition[1]; if (Math.abs(offsetX) <= minOffset.x) { if (Math.abs(offsetX) < minOffset.x) { @@ -826,7 +803,7 @@ export const snapLinearElementPoint = ( nearestSnapsX.push({ type: "point", - points: [currentPointGlobal, referencePoint], + points: [pointerPosition, referencePoint], offset: offsetX, }); @@ -840,7 +817,7 @@ export const snapLinearElementPoint = ( nearestSnapsY.push({ type: "point", - points: [currentPointGlobal, referencePoint], + points: [pointerPosition, referencePoint], offset: offsetY, }); @@ -859,8 +836,8 @@ export const snapLinearElementPoint = ( if (snapOffset.x !== 0 || snapOffset.y !== 0) { // Recalculate snap lines with the snapped position const snappedPosition = pointFrom( - pointPosition.x + snapOffset.x, - pointPosition.y + snapOffset.y, + pointerPosition[0] + snapOffset.x, + pointerPosition[1] + snapOffset.y, ); const snappedSnapsX: Snaps = []; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index c824e53e7c..d744f2fea6 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -5992,7 +5992,7 @@ class App extends React.Component { this.scene.getNonDeletedElements(), multiElement, points.length - 1, - { x: effectiveGridX, y: effectiveGridY }, + pointFrom(effectiveGridX, effectiveGridY), this, event, this.scene.getNonDeletedElementsMap(), @@ -8795,7 +8795,7 @@ class App extends React.Component { this.scene.getNonDeletedElements(), newElement, points.length - 1, - { x: effectiveGridX, y: effectiveGridY }, + pointFrom(effectiveGridX, effectiveGridY), this, event, this.scene.getNonDeletedElementsMap(),