diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 6242404ad..a5352a599 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -8,7 +8,6 @@ import { pointDistance, vectorFromPoint, line, - linesIntersectAt, curveLength, curvePointAtLength, } from "@excalidraw/math"; @@ -29,6 +28,7 @@ import { deconstructLinearOrFreeDrawElement, isPathALoop, snapLinearElementPoint, + snapToDiscreteAngle, type SnapLine, type Store, } from "@excalidraw/element"; @@ -397,45 +397,16 @@ export class LinearElementEditor { pointFrom(referencePointCoords[0], referencePointCoords[1]), ); - const firstSnapLine = snapLines[0]; - if ( - firstSnapLine.type === "points" && - firstSnapLine.points.length > 1 - ) { - const snapLine = line( - firstSnapLine.points[0], - firstSnapLine.points[1], - ); - const intersection = linesIntersectAt( - angleLine, - snapLine, - ); + const result = snapToDiscreteAngle( + snapLines, + angleLine, + pointFrom(gridX, gridY), + referencePointCoords, + ); - if (intersection) { - dxFromReference = intersection[0] - referencePointCoords[0]; - dyFromReference = intersection[1] - referencePointCoords[1]; - - const furthestPoint = firstSnapLine.points.reduce( - (furthest, point) => { - const distance = pointDistance(intersection, point); - if (distance > furthest.distance) { - return { point, distance }; - } - return furthest; - }, - { - point: firstSnapLine.points[0], - distance: pointDistance( - intersection, - firstSnapLine.points[0], - ), - }, - ); - - firstSnapLine.points = [furthestPoint.point, intersection]; - _snapLines = [firstSnapLine]; - } - } + dxFromReference = result.dxFromReference; + dyFromReference = result.dyFromReference; + _snapLines = result.snapLines; } else if (snapLines.length > 0) { const snappedGridX = effectiveGridX + snapOffset.x; const snappedGridY = effectiveGridY + snapOffset.y; @@ -1237,49 +1208,16 @@ export class LinearElementEditor { pointFrom(lastCommittedPointCoords[0], lastCommittedPointCoords[1]), ); - const firstSnapLine = _snapLines[0]; - if ( - firstSnapLine.type === "points" && - firstSnapLine.points.length > 1 - ) { - const snapLine = line( - firstSnapLine.points[0], - firstSnapLine.points[1], - ); - const intersection = linesIntersectAt( - angleLine, - snapLine, - ); + const result = snapToDiscreteAngle( + _snapLines, + angleLine, + pointFrom(gridX, gridY), + lastCommittedPointCoords, + ); - if (intersection) { - dxFromLastCommitted = - intersection[0] - lastCommittedPointCoords[0]; - dyFromLastCommitted = - intersection[1] - lastCommittedPointCoords[1]; - - const furthestPoint = firstSnapLine.points.reduce( - (furthest, point) => { - const distance = pointDistance(intersection, point); - if (distance > furthest.distance) { - return { point, distance }; - } - return furthest; - }, - { - point: firstSnapLine.points[0], - distance: pointDistance( - intersection, - firstSnapLine.points[0], - ), - }, - ); - - firstSnapLine.points = [furthestPoint.point, intersection]; - snapLines = [firstSnapLine]; - } - } else { - snapLines = []; - } + dxFromLastCommitted = result.dxFromReference; + dyFromLastCommitted = result.dyFromReference; + snapLines = result.snapLines; } else if (_snapLines.length > 0) { const snappedGridX = effectiveGridX + snapOffset.x; const snappedGridY = effectiveGridY + snapOffset.y; diff --git a/packages/element/src/snapping.ts b/packages/element/src/snapping.ts index d27b3023b..7a7bc6091 100644 --- a/packages/element/src/snapping.ts +++ b/packages/element/src/snapping.ts @@ -1,5 +1,8 @@ import { isCloseTo, + line, + linesIntersectAt, + pointDistance, pointFrom, pointRotateRads, rangeInclusive, @@ -1643,3 +1646,79 @@ export const isActiveToolNonLinearSnappable = ( activeToolType === TOOL_TYPE.text ); }; + +/** + * Snaps to discrete angle rotation logic. + * This function handles the common pattern of finding intersections between + * angle lines and snap lines, and updating the snap lines accordingly. + * + * @param snapLines - The original snap lines from snapping + * @param angleLine - The line representing the discrete angle constraint + * @param gridPosition - The grid position (original pointer position) + * @param referencePosition - The reference position (usually the start point) + * @returns Object containing updated snap lines and position deltas + */ +export const snapToDiscreteAngle = ( + snapLines: SnapLine[], + angleLine: [GlobalPoint, GlobalPoint], + gridPosition: GlobalPoint, + referencePosition: GlobalPoint, +): { + snapLines: SnapLine[]; + dxFromReference: number; + dyFromReference: number; +} => { + if (snapLines.length === 0) { + return { + snapLines: [], + dxFromReference: gridPosition[0] - referencePosition[0], + dyFromReference: gridPosition[1] - referencePosition[1], + }; + } + + const firstSnapLine = snapLines[0]; + if (firstSnapLine.type === "points" && firstSnapLine.points.length > 1) { + const snapLine = line(firstSnapLine.points[0], firstSnapLine.points[1]); + const intersection = linesIntersectAt( + line(angleLine[0], angleLine[1]), + snapLine, + ); + + if (intersection) { + const dxFromReference = intersection[0] - referencePosition[0]; + const dyFromReference = intersection[1] - referencePosition[1]; + + const furthestPoint = firstSnapLine.points.reduce( + (furthest, point) => { + const distance = pointDistance(intersection, point); + if (distance > furthest.distance) { + return { point, distance }; + } + return furthest; + }, + { + point: firstSnapLine.points[0], + distance: pointDistance(intersection, firstSnapLine.points[0]), + }, + ); + + const updatedSnapLine: PointSnapLine = { + type: "points", + points: [furthestPoint.point, intersection], + }; + + return { + snapLines: [updatedSnapLine], + dxFromReference, + dyFromReference, + }; + } + } + + // If no intersection found, return original snap lines with grid position + return { + snapLines, + dxFromReference: gridPosition[0] - referencePosition[0], + dyFromReference: gridPosition[1] - referencePosition[1], + }; +}; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index c79da532b..b616d5d07 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -17,7 +17,6 @@ import { vectorDot, vectorNormalize, line, - linesIntersectAt, } from "@excalidraw/math"; import { @@ -240,6 +239,7 @@ import { isActiveToolNonLinearSnappable, getSnapLinesAtPointer, snapLinearElementPoint, + snapToDiscreteAngle, isSnappingEnabled, getReferenceSnapPoints, getVisibleGaps, @@ -6030,54 +6030,19 @@ class App extends React.Component { pointFrom(lastCommittedX + rx, lastCommittedY + ry), ); - // Find intersection with first snap line - const firstSnapLine = snapLines[0]; - if ( - firstSnapLine.type === "points" && - firstSnapLine.points.length > 1 - ) { - const snapLine = line( - firstSnapLine.points[0], - firstSnapLine.points[1], - ); - const intersection = linesIntersectAt( - angleLine, - snapLine, - ); + const result = snapToDiscreteAngle( + snapLines, + angleLine, + pointFrom(gridX, gridY), + pointFrom(lastCommittedX + rx, lastCommittedY + ry), + ); - if (intersection) { - dxFromLastCommitted = intersection[0] - rx - lastCommittedX; - dyFromLastCommitted = intersection[1] - ry - lastCommittedY; + dxFromLastCommitted = result.dxFromReference; + dyFromLastCommitted = result.dyFromReference; - // Find the furthest point from the intersection - const furthestPoint = firstSnapLine.points.reduce( - (furthest, point) => { - const distance = pointDistance(intersection, point); - if (distance > furthest.distance) { - return { point, distance }; - } - return furthest; - }, - { - point: firstSnapLine.points[0], - distance: pointDistance( - intersection, - firstSnapLine.points[0], - ), - }, - ); - - firstSnapLine.points = [furthestPoint.point, intersection]; - - this.setState({ - snapLines: [firstSnapLine], - }); - - this.setState({ - snapLines: [firstSnapLine], - }); - } - } + this.setState({ + snapLines: result.snapLines, + }); } else { const snappedGridX = effectiveGridX + snapOffset.x; const snappedGridY = effectiveGridY + snapOffset.y; @@ -8814,52 +8779,19 @@ class App extends React.Component { pointFrom(newElement.x, newElement.y), ); - const firstSnapLine = snapLines.find( - (snapLine) => - snapLine.type === "points" && snapLine.points.length > 2, + const result = snapToDiscreteAngle( + snapLines, + angleLine, + pointFrom(gridX, gridY), + pointFrom(newElement.x, newElement.y), ); - if (firstSnapLine && firstSnapLine.points.length > 1) { - const snapLine = line( - firstSnapLine.points[0], - firstSnapLine.points[1], - ); - const intersection = linesIntersectAt( - angleLine, - snapLine, - ); - if (intersection) { - dx = intersection[0] - newElement.x; - dy = intersection[1] - newElement.y; - // Find the furthest point from the intersection - const furthestPoint = firstSnapLine.points.reduce( - (furthest, point) => { - const distance = pointDistance(intersection, point); - if (distance > furthest.distance) { - return { point, distance }; - } - return furthest; - }, - { - point: firstSnapLine.points[0], - distance: pointDistance( - intersection, - firstSnapLine.points[0], - ), - }, - ); + dx = result.dxFromReference; + dy = result.dyFromReference; - firstSnapLine.points = [furthestPoint.point, intersection]; - - this.setState({ - snapLines: [firstSnapLine], - }); - } else { - this.setState({ - snapLines: [], - }); - } - } + this.setState({ + snapLines: result.snapLines, + }); } else { dx = gridX + snapOffset.x - newElement.x; dy = gridY + snapOffset.y - newElement.y;