fix: do not snap to each other when moving multiple points together

This commit is contained in:
Ryan Di 2025-06-26 17:22:42 +10:00
parent a0f7edadec
commit 0e197ef5c4
3 changed files with 37 additions and 65 deletions

View File

@ -370,17 +370,12 @@ export class LinearElementEditor {
const effectiveGridX = referencePointCoords[0] + dxFromReference; const effectiveGridX = referencePointCoords[0] + dxFromReference;
const effectiveGridY = referencePointCoords[1] + dyFromReference; const effectiveGridY = referencePointCoords[1] + dyFromReference;
let newDraggingPointPosition = pointFrom(
effectiveGridX,
effectiveGridY,
);
if (!isElbowArrow(element)) { if (!isElbowArrow(element)) {
const { snapOffset, snapLines } = snapLinearElementPoint( const { snapOffset, snapLines } = snapLinearElementPoint(
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
element, element,
lastClickedPoint, lastClickedPoint,
{ x: effectiveGridX, y: effectiveGridY }, pointFrom<GlobalPoint>(effectiveGridX, effectiveGridY),
app, app,
event, event,
elementsMap, elementsMap,
@ -448,7 +443,7 @@ export class LinearElementEditor {
-element.angle as Radians, -element.angle as Radians,
); );
newDraggingPointPosition = pointFrom( const newDraggingPointPosition = pointFrom(
referencePoint[0] + rotatedX, referencePoint[0] + rotatedX,
referencePoint[1] + rotatedY, referencePoint[1] + rotatedY,
); );
@ -477,11 +472,11 @@ export class LinearElementEditor {
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
element, element,
lastClickedPoint, lastClickedPoint,
{ x: originalPointerX, y: originalPointerY }, pointFrom(originalPointerX, originalPointerY),
app, app,
event, event,
elementsMap, elementsMap,
{ includeSelfPoints: true }, { includeSelfPoints: true, selectedPointsIndices },
); );
_snapLines = snapLines; _snapLines = snapLines;
@ -1223,7 +1218,7 @@ export class LinearElementEditor {
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
element, element,
points.length - 1, points.length - 1,
{ x: effectiveGridX, y: effectiveGridY }, pointFrom(effectiveGridX, effectiveGridY),
app, app,
event, event,
elementsMap, elementsMap,
@ -1311,7 +1306,7 @@ export class LinearElementEditor {
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
element, element,
points.length - 1, points.length - 1,
{ x: originalPointerX, y: originalPointerY }, pointFrom(originalPointerX, originalPointerY),
app, app,
event, event,
elementsMap, elementsMap,

View File

@ -2,7 +2,6 @@ import {
isCloseTo, isCloseTo,
pointFrom, pointFrom,
pointRotateRads, pointRotateRads,
pointsEqual,
rangeInclusive, rangeInclusive,
rangeIntersection, rangeIntersection,
rangesOverlap, rangesOverlap,
@ -198,13 +197,12 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => {
export const getLinearElementPoints = ( export const getLinearElementPoints = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
elementsMap: ElementsMap,
options: { options: {
dragOffset?: Vector2D; dragOffset?: Vector2D;
excludePointIndex?: number; excludePointsIndices?: readonly number[];
} = {}, } = {},
): GlobalPoint[] => { ): GlobalPoint[] => {
const { dragOffset, excludePointIndex } = options; const { dragOffset, excludePointsIndices } = options;
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
return []; return [];
@ -226,27 +224,25 @@ export const getLinearElementPoints = (
for (let i = 0; i < element.points.length; i++) { for (let i = 0; i < element.points.length; i++) {
// Skip the point being edited if specified // Skip the point being edited if specified
if (excludePointIndex !== undefined && i === excludePointIndex) { if (
excludePointsIndices?.length &&
excludePointsIndices.find((index) => index === i) !== undefined
) {
continue; continue;
} }
const localPoint = element.points[i]; const point = element.points[i];
const globalX = elementX + localPoint[0]; const globalX = elementX + point[0];
const globalY = elementY + localPoint[1]; const globalY = elementY + point[1];
// Apply rotation if element is rotated const cx = elementX + element.width / 2;
if (element.angle !== 0) { const cy = elementY + element.height / 2;
const cx = elementX + element.width / 2; const rotated = pointRotateRads<GlobalPoint>(
const cy = elementY + element.height / 2; pointFrom(globalX, globalY),
const rotated = pointRotateRads<GlobalPoint>( pointFrom(cx, cy),
pointFrom(globalX, globalY), element.angle,
pointFrom(cx, cy), );
element.angle, globalPoints.push(pointFrom(round(rotated[0]), round(rotated[1])));
);
globalPoints.push(pointFrom(round(rotated[0]), round(rotated[1])));
} else {
globalPoints.push(pointFrom(round(globalX), round(globalY)));
}
} }
return globalPoints; return globalPoints;
@ -296,7 +292,7 @@ export const getElementsCorners = (
!boundingBoxCorners !boundingBoxCorners
) { ) {
// For linear elements, use actual points instead of bounding box // For linear elements, use actual points instead of bounding box
const linearPoints = getLinearElementPoints(element, elementsMap, { const linearPoints = getLinearElementPoints(element, {
dragOffset, dragOffset,
}); });
result = linearPoints; result = linearPoints;
@ -714,6 +710,7 @@ export const getReferenceSnapPointsForLinearElementPoint = (
elementsMap: ElementsMap, elementsMap: ElementsMap,
options: { options: {
includeSelfPoints?: boolean; includeSelfPoints?: boolean;
selectedPointsIndices?: readonly number[];
} = {}, } = {},
) => { ) => {
const { includeSelfPoints = false } = options; 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 // Include other points from the same linear element when creating new points or in editing mode
if (includeSelfPoints) { if (includeSelfPoints) {
const elementPoints = getLinearElementPoints(editingElement, elementsMap, { const elementPoints = getLinearElementPoints(editingElement, {
excludePointIndex: editingPointIndex >= 0 ? editingPointIndex : undefined, 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); allSnapPoints.push(...elementPoints);
} }
@ -771,12 +753,13 @@ export const snapLinearElementPoint = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
editingElement: ExcalidrawLinearElement, editingElement: ExcalidrawLinearElement,
editingPointIndex: number, editingPointIndex: number,
pointPosition: Vector2D, pointerPosition: GlobalPoint,
app: AppClassProperties, app: AppClassProperties,
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
elementsMap: ElementsMap, elementsMap: ElementsMap,
options: { options: {
includeSelfPoints?: boolean; includeSelfPoints?: boolean;
selectedPointsIndices?: readonly number[];
} = {}, } = {},
) => { ) => {
if ( if (
@ -808,16 +791,10 @@ export const snapLinearElementPoint = (
options, options,
); );
// Create a snap point for the current point position
const currentPointGlobal = pointFrom<GlobalPoint>(
pointPosition.x,
pointPosition.y,
);
// Find nearest snaps // Find nearest snaps
for (const referencePoint of referenceSnapPoints) { for (const referencePoint of referenceSnapPoints) {
const offsetX = referencePoint[0] - currentPointGlobal[0]; const offsetX = referencePoint[0] - pointerPosition[0];
const offsetY = referencePoint[1] - currentPointGlobal[1]; const offsetY = referencePoint[1] - pointerPosition[1];
if (Math.abs(offsetX) <= minOffset.x) { if (Math.abs(offsetX) <= minOffset.x) {
if (Math.abs(offsetX) < minOffset.x) { if (Math.abs(offsetX) < minOffset.x) {
@ -826,7 +803,7 @@ export const snapLinearElementPoint = (
nearestSnapsX.push({ nearestSnapsX.push({
type: "point", type: "point",
points: [currentPointGlobal, referencePoint], points: [pointerPosition, referencePoint],
offset: offsetX, offset: offsetX,
}); });
@ -840,7 +817,7 @@ export const snapLinearElementPoint = (
nearestSnapsY.push({ nearestSnapsY.push({
type: "point", type: "point",
points: [currentPointGlobal, referencePoint], points: [pointerPosition, referencePoint],
offset: offsetY, offset: offsetY,
}); });
@ -859,8 +836,8 @@ export const snapLinearElementPoint = (
if (snapOffset.x !== 0 || snapOffset.y !== 0) { if (snapOffset.x !== 0 || snapOffset.y !== 0) {
// Recalculate snap lines with the snapped position // Recalculate snap lines with the snapped position
const snappedPosition = pointFrom<GlobalPoint>( const snappedPosition = pointFrom<GlobalPoint>(
pointPosition.x + snapOffset.x, pointerPosition[0] + snapOffset.x,
pointPosition.y + snapOffset.y, pointerPosition[1] + snapOffset.y,
); );
const snappedSnapsX: Snaps = []; const snappedSnapsX: Snaps = [];

View File

@ -5992,7 +5992,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
multiElement, multiElement,
points.length - 1, points.length - 1,
{ x: effectiveGridX, y: effectiveGridY }, pointFrom(effectiveGridX, effectiveGridY),
this, this,
event, event,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
@ -8795,7 +8795,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
newElement, newElement,
points.length - 1, points.length - 1,
{ x: effectiveGridX, y: effectiveGridY }, pointFrom(effectiveGridX, effectiveGridY),
this, this,
event, event,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),