Fix fixed angle orbiting

This commit is contained in:
Mark Tolmacs
2025-07-24 17:38:57 +02:00
parent 57e8734b3f
commit 83004e2c01
3 changed files with 122 additions and 60 deletions

View File

@ -15,7 +15,7 @@ import {
PRECISION,
} from "@excalidraw/math";
import type { LocalPoint, Radians } from "@excalidraw/math";
import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math";
import type { AppState } from "@excalidraw/excalidraw/types";
@ -839,6 +839,7 @@ export const bindPointToSnapToElementOutline = (
bindableElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end",
elementsMap: ElementsMap,
customIntersector?: LineSegment<GlobalPoint>,
): GlobalPoint => {
const aabb = aabbForElement(bindableElement, elementsMap);
const localP =
@ -881,16 +882,18 @@ export const bindPointToSnapToElementOutline = (
isHorizontal ? center[0] : snapPoint[0],
!isHorizontal ? center[1] : snapPoint[1],
);
const intersector = lineSegment(
otherPoint,
pointFromVector(
vectorScale(
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
Math.max(bindableElement.width, bindableElement.height) * 2,
),
const intersector =
customIntersector ??
lineSegment(
otherPoint,
),
);
pointFromVector(
vectorScale(
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
Math.max(bindableElement.width, bindableElement.height) * 2,
),
otherPoint,
),
);
intersection = intersectElementWithLineSegment(
bindableElement,
elementsMap,
@ -898,9 +901,8 @@ export const bindPointToSnapToElementOutline = (
FIXED_BINDING_DISTANCE,
).sort(pointDistanceSq)[0];
} else {
intersection = intersectElementWithLineSegment(
bindableElement,
elementsMap,
const intersector =
customIntersector ??
lineSegment(
adjacentPoint,
pointFromVector(
@ -911,7 +913,11 @@ export const bindPointToSnapToElementOutline = (
),
adjacentPoint,
),
),
);
intersection = intersectElementWithLineSegment(
bindableElement,
elementsMap,
intersector,
FIXED_BINDING_DISTANCE,
).sort(
(g, h) =>
@ -936,6 +942,7 @@ export const getOutlineAvoidingPoint = (
coords: GlobalPoint,
pointIndex: number,
elementsMap: ElementsMap,
customIntersector?: LineSegment<GlobalPoint>,
): GlobalPoint => {
if (hoveredElement) {
const newPoints = Array.from(element.points);
@ -952,6 +959,7 @@ export const getOutlineAvoidingPoint = (
hoveredElement,
pointIndex === 0 ? "start" : "end",
elementsMap,
customIntersector,
);
}

View File

@ -9,6 +9,7 @@ import {
vectorFromPoint,
curveLength,
curvePointAtLength,
lineSegment,
} from "@excalidraw/math";
import { getCurvePathOps } from "@excalidraw/utils/shape";
@ -322,6 +323,8 @@ export class LinearElementEditor {
const draggingPoint = element.points[lastClickedPoint];
if (selectedPointsIndices && draggingPoint) {
const elements = app.scene.getNonDeletedElements();
if (
shouldRotateWithDiscreteAngle(event) &&
selectedPointsIndices.length === 1 &&
@ -336,7 +339,6 @@ export class LinearElementEditor {
element.points[selectedIndex][1] - referencePoint[1],
element.points[selectedIndex][0] - referencePoint[0],
);
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
elementsMap,
@ -345,22 +347,32 @@ export class LinearElementEditor {
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
customLineAngle,
);
const [x, y] = LinearElementEditor.getPointGlobalCoordinates(
element,
pointFrom<LocalPoint>(
width + referencePoint[0],
height + referencePoint[1],
),
elementsMap,
);
LinearElementEditor.movePoints(
element,
app.scene,
new Map([
[
selectedIndex,
{
point: pointFrom(
width + referencePoint[0],
height + referencePoint[1],
),
isDragging: selectedIndex === lastClickedPoint,
},
],
]),
pointDraggingUpdates(
selectedPointsIndices,
0,
0,
elementsMap,
lastClickedPoint,
element,
x,
y,
linearElementEditor,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
elements,
app,
true,
),
);
} else {
const newDraggingPointPosition = LinearElementEditor.createPointAt(
@ -372,7 +384,6 @@ export class LinearElementEditor {
);
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
const elements = app.scene.getNonDeletedElements();
LinearElementEditor.movePoints(
element,
@ -995,7 +1006,6 @@ export class LinearElementEditor {
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
const lastCommittedPoint = points[points.length - 2];
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
elementsMap,
@ -1937,6 +1947,7 @@ const pointDraggingUpdates = (
gridSize: NullableGridSize,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
app: AppClassProperties,
angleLocked?: boolean,
): PointsPositionUpdates => {
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap, true);
const hasMidPoints =
@ -2001,12 +2012,29 @@ const pointDraggingUpdates = (
app.state.bindMode === "orbit" &&
!otherPointInsideElement
) {
let customIntersector;
if (angleLocked) {
const adjacentPointIndex =
pointIndex === 0 ? 1 : element.points.length - 2;
const globalAdjacentPoint =
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
adjacentPointIndex,
elementsMap,
);
customIntersector = lineSegment<GlobalPoint>(
globalAdjacentPoint,
newGlobalPointPosition,
);
}
newGlobalPointPosition = getOutlineAvoidingPoint(
element,
hoveredElement,
newGlobalPointPosition,
pointIndex,
elementsMap,
customIntersector,
);
}

View File

@ -17,6 +17,7 @@ import {
vectorDot,
vectorNormalize,
pointsEqual,
lineSegment,
} from "@excalidraw/math";
import {
@ -6132,11 +6133,29 @@ class App extends React.Component<AppProps, AppState> {
{ informMutation: false, isDragging: false },
);
} else {
let [gridX, gridY] = getGridPoint(
const [lastCommittedX, lastCommittedY] =
multiElement?.lastCommittedPoint ?? [0, 0];
// Handle grid snapping
const [gridX, gridY] = getGridPoint(
scenePointerX,
scenePointerY,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
);
let dxFromLastCommitted = gridX - rx - lastCommittedX;
let dyFromLastCommitted = gridY - ry - lastCommittedY;
if (shouldRotateWithDiscreteAngle(event)) {
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
getLockedLinearCursorAlignSize(
// actual coordinate of the last committed point
lastCommittedX + rx,
lastCommittedY + ry,
// cursor-grid coordinate
gridX,
gridY,
));
}
if (
isArrowElement(multiElement) &&
@ -6172,38 +6191,31 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
multiElement.points.length - 1,
this.scene.getNonDeletedElementsMap(),
shouldRotateWithDiscreteAngle(event)
? lineSegment<GlobalPoint>(
otherPoint,
pointFrom<GlobalPoint>(
multiElement.x + lastCommittedX + dxFromLastCommitted,
multiElement.y + lastCommittedY + dyFromLastCommitted,
),
)
: undefined,
);
gridX = avoidancePoint
const x = avoidancePoint
? avoidancePoint[0]
: hoveredElement
? scenePointerX
: gridX;
gridY = avoidancePoint
const y = avoidancePoint
? avoidancePoint[1]
: hoveredElement
? scenePointerY
: gridY;
dxFromLastCommitted = x - rx - lastCommittedX;
dyFromLastCommitted = y - ry - lastCommittedY;
}
}
const [lastCommittedX, lastCommittedY] =
multiElement?.lastCommittedPoint ?? [0, 0];
let dxFromLastCommitted = gridX - rx - lastCommittedX;
let dyFromLastCommitted = gridY - ry - lastCommittedY;
if (shouldRotateWithDiscreteAngle(event)) {
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
getLockedLinearCursorAlignSize(
// actual coordinate of the last committed point
lastCommittedX + rx,
lastCommittedY + ry,
// cursor-grid coordinate
gridX,
gridY,
));
}
if (isPathALoop(points, this.state.zoom.value)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
}
@ -9089,6 +9101,15 @@ class App extends React.Component<AppProps, AppState> {
let dx = gridX - newElement.x;
let dy = gridY - newElement.y;
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
newElement.x,
newElement.y,
pointerCoords.x,
pointerCoords.y,
));
}
if (
!isElbowArrow(newElement) &&
this.state.editingLinearElement &&
@ -9119,6 +9140,20 @@ class App extends React.Component<AppProps, AppState> {
: pointFrom(gridX, gridY),
newElement.points.length - 1,
elementsMap,
shouldRotateWithDiscreteAngle(event) &&
points.length === 2
? lineSegment(
LinearElementEditor.getPointGlobalCoordinates(
newElement,
points[0],
elementsMap,
),
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
)
: undefined,
)
: pointFrom(gridX, gridY);
@ -9176,15 +9211,6 @@ class App extends React.Component<AppProps, AppState> {
}
}
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
newElement.x,
newElement.y,
pointerCoords.x,
pointerCoords.y,
));
}
if (points.length === 1) {
this.scene.mutateElement(
newElement,