mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Fix fixed angle orbiting
This commit is contained in:
@ -15,7 +15,7 @@ import {
|
|||||||
PRECISION,
|
PRECISION,
|
||||||
} from "@excalidraw/math";
|
} 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";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
@ -839,6 +839,7 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
customIntersector?: LineSegment<GlobalPoint>,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const aabb = aabbForElement(bindableElement, elementsMap);
|
const aabb = aabbForElement(bindableElement, elementsMap);
|
||||||
const localP =
|
const localP =
|
||||||
@ -881,16 +882,18 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
isHorizontal ? center[0] : snapPoint[0],
|
isHorizontal ? center[0] : snapPoint[0],
|
||||||
!isHorizontal ? center[1] : snapPoint[1],
|
!isHorizontal ? center[1] : snapPoint[1],
|
||||||
);
|
);
|
||||||
const intersector = lineSegment(
|
const intersector =
|
||||||
otherPoint,
|
customIntersector ??
|
||||||
pointFromVector(
|
lineSegment(
|
||||||
vectorScale(
|
|
||||||
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
|
|
||||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
|
||||||
),
|
|
||||||
otherPoint,
|
otherPoint,
|
||||||
),
|
pointFromVector(
|
||||||
);
|
vectorScale(
|
||||||
|
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
|
||||||
|
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||||
|
),
|
||||||
|
otherPoint,
|
||||||
|
),
|
||||||
|
);
|
||||||
intersection = intersectElementWithLineSegment(
|
intersection = intersectElementWithLineSegment(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -898,9 +901,8 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
).sort(pointDistanceSq)[0];
|
).sort(pointDistanceSq)[0];
|
||||||
} else {
|
} else {
|
||||||
intersection = intersectElementWithLineSegment(
|
const intersector =
|
||||||
bindableElement,
|
customIntersector ??
|
||||||
elementsMap,
|
|
||||||
lineSegment(
|
lineSegment(
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
pointFromVector(
|
pointFromVector(
|
||||||
@ -911,7 +913,11 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
),
|
),
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
intersection = intersectElementWithLineSegment(
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
intersector,
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
).sort(
|
).sort(
|
||||||
(g, h) =>
|
(g, h) =>
|
||||||
@ -936,6 +942,7 @@ export const getOutlineAvoidingPoint = (
|
|||||||
coords: GlobalPoint,
|
coords: GlobalPoint,
|
||||||
pointIndex: number,
|
pointIndex: number,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
customIntersector?: LineSegment<GlobalPoint>,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (hoveredElement) {
|
if (hoveredElement) {
|
||||||
const newPoints = Array.from(element.points);
|
const newPoints = Array.from(element.points);
|
||||||
@ -952,6 +959,7 @@ export const getOutlineAvoidingPoint = (
|
|||||||
hoveredElement,
|
hoveredElement,
|
||||||
pointIndex === 0 ? "start" : "end",
|
pointIndex === 0 ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
customIntersector,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
curveLength,
|
curveLength,
|
||||||
curvePointAtLength,
|
curvePointAtLength,
|
||||||
|
lineSegment,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||||
@ -322,6 +323,8 @@ export class LinearElementEditor {
|
|||||||
const draggingPoint = element.points[lastClickedPoint];
|
const draggingPoint = element.points[lastClickedPoint];
|
||||||
|
|
||||||
if (selectedPointsIndices && draggingPoint) {
|
if (selectedPointsIndices && draggingPoint) {
|
||||||
|
const elements = app.scene.getNonDeletedElements();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldRotateWithDiscreteAngle(event) &&
|
shouldRotateWithDiscreteAngle(event) &&
|
||||||
selectedPointsIndices.length === 1 &&
|
selectedPointsIndices.length === 1 &&
|
||||||
@ -336,7 +339,6 @@ export class LinearElementEditor {
|
|||||||
element.points[selectedIndex][1] - referencePoint[1],
|
element.points[selectedIndex][1] - referencePoint[1],
|
||||||
element.points[selectedIndex][0] - referencePoint[0],
|
element.points[selectedIndex][0] - referencePoint[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -345,22 +347,32 @@ export class LinearElementEditor {
|
|||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
customLineAngle,
|
customLineAngle,
|
||||||
);
|
);
|
||||||
|
const [x, y] = LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
pointFrom<LocalPoint>(
|
||||||
|
width + referencePoint[0],
|
||||||
|
height + referencePoint[1],
|
||||||
|
),
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
app.scene,
|
app.scene,
|
||||||
new Map([
|
pointDraggingUpdates(
|
||||||
[
|
selectedPointsIndices,
|
||||||
selectedIndex,
|
0,
|
||||||
{
|
0,
|
||||||
point: pointFrom(
|
elementsMap,
|
||||||
width + referencePoint[0],
|
lastClickedPoint,
|
||||||
height + referencePoint[1],
|
element,
|
||||||
),
|
x,
|
||||||
isDragging: selectedIndex === lastClickedPoint,
|
y,
|
||||||
},
|
linearElementEditor,
|
||||||
],
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
]),
|
elements,
|
||||||
|
app,
|
||||||
|
true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||||
@ -372,7 +384,6 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||||
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||||
const elements = app.scene.getNonDeletedElements();
|
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
@ -995,7 +1006,6 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
||||||
const lastCommittedPoint = points[points.length - 2];
|
const lastCommittedPoint = points[points.length - 2];
|
||||||
|
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -1937,6 +1947,7 @@ const pointDraggingUpdates = (
|
|||||||
gridSize: NullableGridSize,
|
gridSize: NullableGridSize,
|
||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
|
angleLocked?: boolean,
|
||||||
): PointsPositionUpdates => {
|
): PointsPositionUpdates => {
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap, true);
|
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap, true);
|
||||||
const hasMidPoints =
|
const hasMidPoints =
|
||||||
@ -2001,12 +2012,29 @@ const pointDraggingUpdates = (
|
|||||||
app.state.bindMode === "orbit" &&
|
app.state.bindMode === "orbit" &&
|
||||||
!otherPointInsideElement
|
!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(
|
newGlobalPointPosition = getOutlineAvoidingPoint(
|
||||||
element,
|
element,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
newGlobalPointPosition,
|
newGlobalPointPosition,
|
||||||
pointIndex,
|
pointIndex,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
customIntersector,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
vectorDot,
|
vectorDot,
|
||||||
vectorNormalize,
|
vectorNormalize,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
|
lineSegment,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -6132,11 +6133,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
{ informMutation: false, isDragging: false },
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let [gridX, gridY] = getGridPoint(
|
const [lastCommittedX, lastCommittedY] =
|
||||||
|
multiElement?.lastCommittedPoint ?? [0, 0];
|
||||||
|
|
||||||
|
// Handle grid snapping
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
scenePointerX,
|
scenePointerX,
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
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 (
|
if (
|
||||||
isArrowElement(multiElement) &&
|
isArrowElement(multiElement) &&
|
||||||
@ -6172,38 +6191,31 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||||
multiElement.points.length - 1,
|
multiElement.points.length - 1,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
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]
|
? avoidancePoint[0]
|
||||||
: hoveredElement
|
: hoveredElement
|
||||||
? scenePointerX
|
? scenePointerX
|
||||||
: gridX;
|
: gridX;
|
||||||
gridY = avoidancePoint
|
const y = avoidancePoint
|
||||||
? avoidancePoint[1]
|
? avoidancePoint[1]
|
||||||
: hoveredElement
|
: hoveredElement
|
||||||
? scenePointerY
|
? scenePointerY
|
||||||
: gridY;
|
: 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)) {
|
if (isPathALoop(points, this.state.zoom.value)) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
}
|
}
|
||||||
@ -9089,6 +9101,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let dx = gridX - newElement.x;
|
let dx = gridX - newElement.x;
|
||||||
let dy = gridY - newElement.y;
|
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 (
|
if (
|
||||||
!isElbowArrow(newElement) &&
|
!isElbowArrow(newElement) &&
|
||||||
this.state.editingLinearElement &&
|
this.state.editingLinearElement &&
|
||||||
@ -9119,6 +9140,20 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: pointFrom(gridX, gridY),
|
: pointFrom(gridX, gridY),
|
||||||
newElement.points.length - 1,
|
newElement.points.length - 1,
|
||||||
elementsMap,
|
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);
|
: 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) {
|
if (points.length === 1) {
|
||||||
this.scene.mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
|
Reference in New Issue
Block a user