Compare commits
7 Commits
master
...
ryan-di/li
Author | SHA1 | Date | |
---|---|---|---|
|
0e197ef5c4 | ||
|
a0f7edadec | ||
|
58c9bb4712 | ||
|
d1c6304d42 | ||
|
c1a54455bb | ||
|
07640dd756 | ||
|
5403fa8a0d |
@ -105,6 +105,7 @@ export * from "./selection";
|
|||||||
export * from "./shape";
|
export * from "./shape";
|
||||||
export * from "./showSelectedShapeActions";
|
export * from "./showSelectedShapeActions";
|
||||||
export * from "./sizeHelpers";
|
export * from "./sizeHelpers";
|
||||||
|
export * from "./snapping";
|
||||||
export * from "./sortElements";
|
export * from "./sortElements";
|
||||||
export * from "./store";
|
export * from "./store";
|
||||||
export * from "./textElement";
|
export * from "./textElement";
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
|
line,
|
||||||
|
linesIntersectAt,
|
||||||
curveLength,
|
curveLength,
|
||||||
curvePointAtLength,
|
curvePointAtLength,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
@ -26,6 +28,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
deconstructLinearOrFreeDrawElement,
|
deconstructLinearOrFreeDrawElement,
|
||||||
isPathALoop,
|
isPathALoop,
|
||||||
|
snapLinearElementPoint,
|
||||||
|
type SnapLine,
|
||||||
type Store,
|
type Store,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
@ -314,9 +318,10 @@ export class LinearElementEditor {
|
|||||||
: 0
|
: 0
|
||||||
: linearElementEditor.pointerDownState.lastClickedPoint;
|
: linearElementEditor.pointerDownState.lastClickedPoint;
|
||||||
|
|
||||||
// point that's being dragged (out of all selected points)
|
|
||||||
const draggingPoint = element.points[lastClickedPoint];
|
const draggingPoint = element.points[lastClickedPoint];
|
||||||
|
|
||||||
|
let _snapLines: SnapLine[] = [];
|
||||||
|
|
||||||
if (selectedPointsIndices && draggingPoint) {
|
if (selectedPointsIndices && draggingPoint) {
|
||||||
if (
|
if (
|
||||||
shouldRotateWithDiscreteAngle(event) &&
|
shouldRotateWithDiscreteAngle(event) &&
|
||||||
@ -333,13 +338,114 @@ export class LinearElementEditor {
|
|||||||
element.points[selectedIndex][0] - referencePoint[0],
|
element.points[selectedIndex][0] - referencePoint[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
const referencePointCoords =
|
||||||
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
|
||||||
referencePoint,
|
referencePoint,
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
elementsMap,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
);
|
||||||
|
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
||||||
|
? null
|
||||||
|
: app.getEffectiveGridSize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let dxFromReference = gridX - referencePointCoords[0];
|
||||||
|
let dyFromReference = gridY - referencePointCoords[1];
|
||||||
|
|
||||||
|
if (shouldRotateWithDiscreteAngle(event)) {
|
||||||
|
({ width: dxFromReference, height: dyFromReference } =
|
||||||
|
getLockedLinearCursorAlignSize(
|
||||||
|
referencePointCoords[0],
|
||||||
|
referencePointCoords[1],
|
||||||
|
gridX,
|
||||||
|
gridY,
|
||||||
customLineAngle,
|
customLineAngle,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveGridX = referencePointCoords[0] + dxFromReference;
|
||||||
|
const effectiveGridY = referencePointCoords[1] + dyFromReference;
|
||||||
|
|
||||||
|
if (!isElbowArrow(element)) {
|
||||||
|
const { snapOffset, snapLines } = snapLinearElementPoint(
|
||||||
|
app.scene.getNonDeletedElements(),
|
||||||
|
element,
|
||||||
|
lastClickedPoint,
|
||||||
|
pointFrom<GlobalPoint>(effectiveGridX, effectiveGridY),
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
elementsMap,
|
||||||
|
{ includeSelfPoints: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
_snapLines = snapLines;
|
||||||
|
|
||||||
|
if (snapLines.length > 0 && shouldRotateWithDiscreteAngle(event)) {
|
||||||
|
const angleLine = line<GlobalPoint>(
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
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<GlobalPoint>(
|
||||||
|
angleLine,
|
||||||
|
snapLine,
|
||||||
|
);
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (snapLines.length > 0) {
|
||||||
|
const snappedGridX = effectiveGridX + snapOffset.x;
|
||||||
|
const snappedGridY = effectiveGridY + snapOffset.y;
|
||||||
|
dxFromReference = snappedGridX - referencePointCoords[0];
|
||||||
|
dyFromReference = snappedGridY - referencePointCoords[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
|
pointFrom(dxFromReference, dyFromReference),
|
||||||
|
pointFrom(0, 0),
|
||||||
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newDraggingPointPosition = pointFrom(
|
||||||
|
referencePoint[0] + rotatedX,
|
||||||
|
referencePoint[1] + rotatedY,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
@ -349,21 +455,41 @@ export class LinearElementEditor {
|
|||||||
[
|
[
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
{
|
{
|
||||||
point: pointFrom(
|
point: newDraggingPointPosition,
|
||||||
width + referencePoint[0],
|
|
||||||
height + referencePoint[1],
|
|
||||||
),
|
|
||||||
isDragging: selectedIndex === lastClickedPoint,
|
isDragging: selectedIndex === lastClickedPoint,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]),
|
]) as PointsPositionUpdates,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Apply object snapping for the point being dragged
|
||||||
|
const originalPointerX =
|
||||||
|
scenePointerX - linearElementEditor.pointerOffset.x;
|
||||||
|
const originalPointerY =
|
||||||
|
scenePointerY - linearElementEditor.pointerOffset.y;
|
||||||
|
|
||||||
|
const { snapOffset, snapLines } = snapLinearElementPoint(
|
||||||
|
app.scene.getNonDeletedElements(),
|
||||||
|
element,
|
||||||
|
lastClickedPoint,
|
||||||
|
pointFrom(originalPointerX, originalPointerY),
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
elementsMap,
|
||||||
|
{ includeSelfPoints: true, selectedPointsIndices },
|
||||||
|
);
|
||||||
|
|
||||||
|
_snapLines = snapLines;
|
||||||
|
|
||||||
|
// Apply snap offset to get final coordinates
|
||||||
|
const snappedPointerX = originalPointerX + snapOffset.x;
|
||||||
|
const snappedPointerY = originalPointerY + snapOffset.y;
|
||||||
|
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
snappedPointerX,
|
||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
snappedPointerY,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -377,15 +503,7 @@ export class LinearElementEditor {
|
|||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
const newPointPosition: LocalPoint =
|
const newPointPosition: LocalPoint =
|
||||||
pointIndex === lastClickedPoint
|
pointIndex === lastClickedPoint
|
||||||
? LinearElementEditor.createPointAt(
|
? newDraggingPointPosition
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
|
||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
|
||||||
event[KEYS.CTRL_OR_CMD]
|
|
||||||
? null
|
|
||||||
: app.getEffectiveGridSize(),
|
|
||||||
)
|
|
||||||
: pointFrom(
|
: pointFrom(
|
||||||
element.points[pointIndex][0] + deltaX,
|
element.points[pointIndex][0] + deltaX,
|
||||||
element.points[pointIndex][1] + deltaY,
|
element.points[pointIndex][1] + deltaY,
|
||||||
@ -484,6 +602,7 @@ export class LinearElementEditor {
|
|||||||
: null,
|
: null,
|
||||||
selectedLinearElement: newLinearElementEditor,
|
selectedLinearElement: newLinearElementEditor,
|
||||||
suggestedBindings,
|
suggestedBindings,
|
||||||
|
snapLines: _snapLines,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1021,7 +1140,10 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
): LinearElementEditor | null {
|
): {
|
||||||
|
linearElementEditor: LinearElementEditor;
|
||||||
|
snapLines: readonly SnapLine[];
|
||||||
|
} | null {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
if (!appState.editingLinearElement) {
|
if (!appState.editingLinearElement) {
|
||||||
return null;
|
return null;
|
||||||
@ -1030,7 +1152,10 @@ export class LinearElementEditor {
|
|||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return appState.editingLinearElement;
|
return {
|
||||||
|
linearElementEditor: appState.editingLinearElement,
|
||||||
|
snapLines: appState.snapLines,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
@ -1041,34 +1166,163 @@ export class LinearElementEditor {
|
|||||||
LinearElementEditor.deletePoints(element, app, [points.length - 1]);
|
LinearElementEditor.deletePoints(element, app, [points.length - 1]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
linearElementEditor: {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
lastUncommittedPoint: null,
|
lastUncommittedPoint: null,
|
||||||
|
isDragging: false,
|
||||||
|
pointerOffset: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
snapLines: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let newPoint: LocalPoint;
|
let newPoint: LocalPoint;
|
||||||
|
let snapLines: SnapLine[] = [];
|
||||||
|
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
||||||
|
? null
|
||||||
|
: app.getEffectiveGridSize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [lastCommittedX, lastCommittedY] = points[points.length - 2] ?? [
|
||||||
|
0, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
const lastCommittedPointCoords =
|
||||||
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
pointFrom(lastCommittedX, lastCommittedY),
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
let dxFromLastCommitted = gridX - lastCommittedPointCoords[0];
|
||||||
|
let dyFromLastCommitted = gridY - lastCommittedPointCoords[1];
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
||||||
const lastCommittedPoint = points[points.length - 2];
|
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
||||||
|
getLockedLinearCursorAlignSize(
|
||||||
|
lastCommittedPointCoords[0],
|
||||||
|
lastCommittedPointCoords[1],
|
||||||
|
gridX,
|
||||||
|
gridY,
|
||||||
|
));
|
||||||
|
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
const effectiveGridX = lastCommittedPointCoords[0] + dxFromLastCommitted;
|
||||||
|
const effectiveGridY = lastCommittedPointCoords[1] + dyFromLastCommitted;
|
||||||
|
|
||||||
|
if (!isElbowArrow(element)) {
|
||||||
|
const { snapOffset, snapLines: _snapLines } = snapLinearElementPoint(
|
||||||
|
app.scene.getNonDeletedElements(),
|
||||||
element,
|
element,
|
||||||
|
points.length - 1,
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
app,
|
||||||
|
event,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
lastCommittedPoint,
|
{ includeSelfPoints: true },
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
);
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
|
||||||
|
snapLines = _snapLines;
|
||||||
|
|
||||||
|
if (_snapLines.length > 0 && shouldRotateWithDiscreteAngle(event)) {
|
||||||
|
const angleLine = line<GlobalPoint>(
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
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<GlobalPoint>(
|
||||||
|
angleLine,
|
||||||
|
snapLine,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
|
} else if (_snapLines.length > 0) {
|
||||||
|
const snappedGridX = effectiveGridX + snapOffset.x;
|
||||||
|
const snappedGridY = effectiveGridY + snapOffset.y;
|
||||||
|
dxFromLastCommitted = snappedGridX - lastCommittedPointCoords[0];
|
||||||
|
dyFromLastCommitted = snappedGridY - lastCommittedPointCoords[1];
|
||||||
|
} else {
|
||||||
|
snapLines = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
|
pointFrom(dxFromLastCommitted, dyFromLastCommitted),
|
||||||
|
pointFrom(0, 0),
|
||||||
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
newPoint = pointFrom(
|
newPoint = pointFrom(
|
||||||
width + lastCommittedPoint[0],
|
lastCommittedX + rotatedX,
|
||||||
height + lastCommittedPoint[1],
|
lastCommittedY + rotatedY,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const originalPointerX =
|
||||||
|
scenePointerX - appState.editingLinearElement.pointerOffset.x;
|
||||||
|
const originalPointerY =
|
||||||
|
scenePointerY - appState.editingLinearElement.pointerOffset.y;
|
||||||
|
|
||||||
|
const { snapOffset, snapLines: snappingLines } = snapLinearElementPoint(
|
||||||
|
app.scene.getNonDeletedElements(),
|
||||||
|
element,
|
||||||
|
points.length - 1,
|
||||||
|
pointFrom(originalPointerX, originalPointerY),
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
elementsMap,
|
||||||
|
{ includeSelfPoints: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
snapLines = snappingLines;
|
||||||
|
|
||||||
|
const snappedPointerX = originalPointerX + snapOffset.x;
|
||||||
|
const snappedPointerY = originalPointerY + snapOffset.y;
|
||||||
|
|
||||||
newPoint = LinearElementEditor.createPointAt(
|
newPoint = LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
scenePointerX - appState.editingLinearElement.pointerOffset.x,
|
snappedPointerX,
|
||||||
scenePointerY - appState.editingLinearElement.pointerOffset.y,
|
snappedPointerY,
|
||||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
||||||
? null
|
? null
|
||||||
: app.getEffectiveGridSize(),
|
: app.getEffectiveGridSize(),
|
||||||
@ -1081,7 +1335,7 @@ export class LinearElementEditor {
|
|||||||
app.scene,
|
app.scene,
|
||||||
new Map([
|
new Map([
|
||||||
[
|
[
|
||||||
element.points.length - 1,
|
points.length - 1,
|
||||||
{
|
{
|
||||||
point: newPoint,
|
point: newPoint,
|
||||||
},
|
},
|
||||||
@ -1091,9 +1345,13 @@ export class LinearElementEditor {
|
|||||||
} else {
|
} else {
|
||||||
LinearElementEditor.addPoints(element, app.scene, [newPoint]);
|
LinearElementEditor.addPoints(element, app.scene, [newPoint]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
linearElementEditor: {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
lastUncommittedPoint: element.points[element.points.length - 1],
|
lastUncommittedPoint: element.points[element.points.length - 1],
|
||||||
|
},
|
||||||
|
snapLines,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
isCloseTo,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
rangeInclusive,
|
rangeInclusive,
|
||||||
@ -13,7 +14,11 @@ import {
|
|||||||
getDraggedElementsBounds,
|
getDraggedElementsBounds,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
import { isBoundToContainer, isFrameLikeElement } from "@excalidraw/element";
|
import {
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameLikeElement,
|
||||||
|
isElbowArrow,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { getMaximumGroups } from "@excalidraw/element";
|
import { getMaximumGroups } from "@excalidraw/element";
|
||||||
|
|
||||||
@ -29,6 +34,7 @@ import type { MaybeTransformHandleType } from "@excalidraw/element";
|
|||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
@ -36,7 +42,7 @@ import type {
|
|||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppState,
|
AppState,
|
||||||
KeyboardModifiersObject,
|
KeyboardModifiersObject,
|
||||||
} from "./types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
const SNAP_DISTANCE = 8;
|
const SNAP_DISTANCE = 8;
|
||||||
|
|
||||||
@ -189,6 +195,59 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => {
|
|||||||
return Math.abs(a - b) <= precision;
|
return Math.abs(a - b) <= precision;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getLinearElementPoints = (
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
options: {
|
||||||
|
dragOffset?: Vector2D;
|
||||||
|
excludePointsIndices?: readonly number[];
|
||||||
|
} = {},
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
const { dragOffset, excludePointsIndices } = options;
|
||||||
|
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element.points || element.points.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let elementX = element.x;
|
||||||
|
let elementY = element.y;
|
||||||
|
|
||||||
|
if (dragOffset) {
|
||||||
|
elementX += dragOffset.x;
|
||||||
|
elementY += dragOffset.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalPoints: GlobalPoint[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < element.points.length; i++) {
|
||||||
|
// Skip the point being edited if specified
|
||||||
|
if (
|
||||||
|
excludePointsIndices?.length &&
|
||||||
|
excludePointsIndices.find((index) => index === i) !== undefined
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = element.points[i];
|
||||||
|
const globalX = elementX + point[0];
|
||||||
|
const globalY = elementY + point[1];
|
||||||
|
|
||||||
|
const cx = elementX + element.width / 2;
|
||||||
|
const cy = elementY + element.height / 2;
|
||||||
|
const rotated = pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(globalX, globalY),
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
globalPoints.push(pointFrom(round(rotated[0]), round(rotated[1])));
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalPoints;
|
||||||
|
};
|
||||||
|
|
||||||
export const getElementsCorners = (
|
export const getElementsCorners = (
|
||||||
elements: ExcalidrawElement[],
|
elements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
@ -229,6 +288,15 @@ export const getElementsCorners = (
|
|||||||
const halfHeight = (y2 - y1) / 2;
|
const halfHeight = (y2 - y1) / 2;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
(element.type === "line" || element.type === "arrow") &&
|
||||||
|
!boundingBoxCorners
|
||||||
|
) {
|
||||||
|
// For linear elements, use actual points instead of bounding box
|
||||||
|
const linearPoints = getLinearElementPoints(element, {
|
||||||
|
dragOffset,
|
||||||
|
});
|
||||||
|
result = linearPoints;
|
||||||
|
} else if (
|
||||||
(element.type === "diamond" || element.type === "ellipse") &&
|
(element.type === "diamond" || element.type === "ellipse") &&
|
||||||
!boundingBoxCorners
|
!boundingBoxCorners
|
||||||
) {
|
) {
|
||||||
@ -634,6 +702,179 @@ export const getReferenceSnapPoints = (
|
|||||||
.flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
|
.flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getReferenceSnapPointsForLinearElementPoint = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
editingElement: ExcalidrawLinearElement,
|
||||||
|
editingPointIndex: number,
|
||||||
|
appState: AppState,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
options: {
|
||||||
|
includeSelfPoints?: boolean;
|
||||||
|
selectedPointsIndices?: readonly number[];
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
const { includeSelfPoints = false } = options;
|
||||||
|
|
||||||
|
// Get all reference elements (excluding the one being edited)
|
||||||
|
const referenceElements = getReferenceElements(
|
||||||
|
elements,
|
||||||
|
[editingElement],
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSnapPoints: GlobalPoint[] = [];
|
||||||
|
|
||||||
|
// Add snap points from all reference elements
|
||||||
|
const referenceGroups = getMaximumGroups(
|
||||||
|
referenceElements,
|
||||||
|
elementsMap,
|
||||||
|
).filter(
|
||||||
|
(elementsGroup) =>
|
||||||
|
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const elementGroup of referenceGroups) {
|
||||||
|
allSnapPoints.push(...getElementsCorners(elementGroup, elementsMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include other points from the same linear element when creating new points or in editing mode
|
||||||
|
if (includeSelfPoints) {
|
||||||
|
const elementPoints = getLinearElementPoints(editingElement, {
|
||||||
|
excludePointsIndices: options.selectedPointsIndices,
|
||||||
|
});
|
||||||
|
allSnapPoints.push(...elementPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSnapPoints;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const snapLinearElementPoint = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
editingElement: ExcalidrawLinearElement,
|
||||||
|
editingPointIndex: number,
|
||||||
|
pointerPosition: GlobalPoint,
|
||||||
|
app: AppClassProperties,
|
||||||
|
event: KeyboardModifiersObject,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
options: {
|
||||||
|
includeSelfPoints?: boolean;
|
||||||
|
selectedPointsIndices?: readonly number[];
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!isSnappingEnabled({ app, event, selectedElements: [editingElement] }) ||
|
||||||
|
isElbowArrow(editingElement)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
snapOffset: { x: 0, y: 0 },
|
||||||
|
snapLines: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapDistance = getSnapDistance(app.state.zoom.value);
|
||||||
|
const minOffset = {
|
||||||
|
x: snapDistance,
|
||||||
|
y: snapDistance,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nearestSnapsX: Snaps = [];
|
||||||
|
const nearestSnapsY: Snaps = [];
|
||||||
|
|
||||||
|
// Get reference snap points (all elements except the current point)
|
||||||
|
const referenceSnapPoints = getReferenceSnapPointsForLinearElementPoint(
|
||||||
|
elements,
|
||||||
|
editingElement,
|
||||||
|
editingPointIndex,
|
||||||
|
app.state,
|
||||||
|
elementsMap,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find nearest snaps
|
||||||
|
for (const referencePoint of referenceSnapPoints) {
|
||||||
|
const offsetX = referencePoint[0] - pointerPosition[0];
|
||||||
|
const offsetY = referencePoint[1] - pointerPosition[1];
|
||||||
|
|
||||||
|
if (Math.abs(offsetX) <= minOffset.x) {
|
||||||
|
if (Math.abs(offsetX) < minOffset.x) {
|
||||||
|
nearestSnapsX.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestSnapsX.push({
|
||||||
|
type: "point",
|
||||||
|
points: [pointerPosition, referencePoint],
|
||||||
|
offset: offsetX,
|
||||||
|
});
|
||||||
|
|
||||||
|
minOffset.x = Math.abs(offsetX);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(offsetY) <= minOffset.y) {
|
||||||
|
if (Math.abs(offsetY) < minOffset.y) {
|
||||||
|
nearestSnapsY.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestSnapsY.push({
|
||||||
|
type: "point",
|
||||||
|
points: [pointerPosition, referencePoint],
|
||||||
|
offset: offsetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
minOffset.y = Math.abs(offsetY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapOffset = {
|
||||||
|
x: nearestSnapsX[0]?.offset ?? 0,
|
||||||
|
y: nearestSnapsY[0]?.offset ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create snap lines using the snapped position (fixed position)
|
||||||
|
let pointSnapLines: SnapLine[] = [];
|
||||||
|
|
||||||
|
if (snapOffset.x !== 0 || snapOffset.y !== 0) {
|
||||||
|
// Recalculate snap lines with the snapped position
|
||||||
|
const snappedPosition = pointFrom<GlobalPoint>(
|
||||||
|
pointerPosition[0] + snapOffset.x,
|
||||||
|
pointerPosition[1] + snapOffset.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
const snappedSnapsX: Snaps = [];
|
||||||
|
const snappedSnapsY: Snaps = [];
|
||||||
|
|
||||||
|
// Find the reference points that we're snapping to
|
||||||
|
for (const referencePoint of referenceSnapPoints) {
|
||||||
|
const offsetX = referencePoint[0] - snappedPosition[0];
|
||||||
|
const offsetY = referencePoint[1] - snappedPosition[1];
|
||||||
|
|
||||||
|
// Only include points that we're actually snapping to
|
||||||
|
if (isCloseTo(offsetX, 0, 0.01)) {
|
||||||
|
snappedSnapsX.push({
|
||||||
|
type: "point",
|
||||||
|
points: [snappedPosition, referencePoint],
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloseTo(offsetY, 0, 0.01)) {
|
||||||
|
snappedSnapsY.push({
|
||||||
|
type: "point",
|
||||||
|
points: [snappedPosition, referencePoint],
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pointSnapLines = createPointSnapLines(snappedSnapsX, snappedSnapsY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
snapOffset,
|
||||||
|
snapLines: pointSnapLines,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getPointSnaps = (
|
const getPointSnaps = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
selectionSnapPoints: GlobalPoint[],
|
selectionSnapPoints: GlobalPoint[],
|
@ -16,6 +16,8 @@ import {
|
|||||||
vectorSubtract,
|
vectorSubtract,
|
||||||
vectorDot,
|
vectorDot,
|
||||||
vectorNormalize,
|
vectorNormalize,
|
||||||
|
line,
|
||||||
|
linesIntersectAt,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -232,9 +234,20 @@ import {
|
|||||||
hitElementBoundingBox,
|
hitElementBoundingBox,
|
||||||
isLineElement,
|
isLineElement,
|
||||||
isSimpleArrow,
|
isSimpleArrow,
|
||||||
|
isGridModeEnabled,
|
||||||
|
SnapCache,
|
||||||
|
isActiveToolNonLinearSnappable,
|
||||||
|
getSnapLinesAtPointer,
|
||||||
|
snapLinearElementPoint,
|
||||||
|
isSnappingEnabled,
|
||||||
|
getReferenceSnapPoints,
|
||||||
|
getVisibleGaps,
|
||||||
|
snapDraggedElements,
|
||||||
|
snapNewElement,
|
||||||
|
snapResizingElements,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@ -360,18 +373,6 @@ import {
|
|||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import { editorJotaiStore, type WritableAtom } from "../editor-jotai";
|
import { editorJotaiStore, type WritableAtom } from "../editor-jotai";
|
||||||
import { ImageSceneDataError } from "../errors";
|
import { ImageSceneDataError } from "../errors";
|
||||||
import {
|
|
||||||
getSnapLinesAtPointer,
|
|
||||||
snapDraggedElements,
|
|
||||||
isActiveToolNonLinearSnappable,
|
|
||||||
snapNewElement,
|
|
||||||
snapResizingElements,
|
|
||||||
isSnappingEnabled,
|
|
||||||
getVisibleGaps,
|
|
||||||
getReferenceSnapPoints,
|
|
||||||
SnapCache,
|
|
||||||
isGridModeEnabled,
|
|
||||||
} from "../snapping";
|
|
||||||
import { convertToExcalidrawElements } from "../data/transform";
|
import { convertToExcalidrawElements } from "../data/transform";
|
||||||
import { Renderer } from "../scene/Renderer";
|
import { Renderer } from "../scene/Renderer";
|
||||||
import {
|
import {
|
||||||
@ -5793,9 +5794,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
||||||
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
||||||
|
|
||||||
|
// snap origin of the new element that's to be created
|
||||||
if (
|
if (
|
||||||
!this.state.newElement &&
|
!this.state.newElement &&
|
||||||
isActiveToolNonLinearSnappable(this.state.activeTool.type)
|
(isActiveToolNonLinearSnappable(this.state.activeTool.type) ||
|
||||||
|
((this.state.activeTool.type === "line" ||
|
||||||
|
this.state.activeTool.type === "arrow") &&
|
||||||
|
this.state.currentItemArrowType !== ARROW_TYPE.elbow))
|
||||||
) {
|
) {
|
||||||
const { originOffset, snapLines } = getSnapLinesAtPointer(
|
const { originOffset, snapLines } = getSnapLinesAtPointer(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
@ -5844,13 +5849,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.editingLinearElement &&
|
this.state.editingLinearElement &&
|
||||||
!this.state.editingLinearElement.isDragging
|
!this.state.editingLinearElement.isDragging
|
||||||
) {
|
) {
|
||||||
const editingLinearElement = LinearElementEditor.handlePointerMove(
|
const result = LinearElementEditor.handlePointerMove(
|
||||||
event,
|
event,
|
||||||
scenePointerX,
|
scenePointerX,
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const { linearElementEditor: editingLinearElement, snapLines } = result;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
editingLinearElement &&
|
editingLinearElement &&
|
||||||
editingLinearElement !== this.state.editingLinearElement
|
editingLinearElement !== this.state.editingLinearElement
|
||||||
@ -5861,6 +5869,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
editingLinearElement,
|
editingLinearElement,
|
||||||
|
snapLines,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -5876,6 +5885,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isBindingElementType(this.state.activeTool.type)) {
|
if (isBindingElementType(this.state.activeTool.type)) {
|
||||||
// Hovering with a selected tool or creating new linear element via click
|
// Hovering with a selected tool or creating new linear element via click
|
||||||
@ -5960,7 +5970,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let dxFromLastCommitted = gridX - rx - lastCommittedX;
|
let dxFromLastCommitted = gridX - rx - lastCommittedX;
|
||||||
let dyFromLastCommitted = gridY - ry - lastCommittedY;
|
let dyFromLastCommitted = gridY - ry - lastCommittedY;
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event)) {
|
const rotateWithDiscreteAngle = shouldRotateWithDiscreteAngle(event);
|
||||||
|
|
||||||
|
if (rotateWithDiscreteAngle) {
|
||||||
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
||||||
getLockedLinearCursorAlignSize(
|
getLockedLinearCursorAlignSize(
|
||||||
// actual coordinate of the last committed point
|
// actual coordinate of the last committed point
|
||||||
@ -5972,10 +5984,97 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const effectiveGridX = lastCommittedX + dxFromLastCommitted + rx;
|
||||||
|
const effectiveGridY = lastCommittedY + dyFromLastCommitted + ry;
|
||||||
|
|
||||||
|
if (!isElbowArrow(multiElement)) {
|
||||||
|
const { snapOffset, snapLines } = snapLinearElementPoint(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
multiElement,
|
||||||
|
points.length - 1,
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
this,
|
||||||
|
event,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
{ includeSelfPoints: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snapLines.length > 0) {
|
||||||
|
if (rotateWithDiscreteAngle) {
|
||||||
|
// Create line from effective position to last committed point
|
||||||
|
const angleLine = line<GlobalPoint>(
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
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<GlobalPoint>(
|
||||||
|
angleLine,
|
||||||
|
snapLine,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intersection) {
|
||||||
|
dxFromLastCommitted = intersection[0] - rx - lastCommittedX;
|
||||||
|
dyFromLastCommitted = intersection[1] - ry - lastCommittedY;
|
||||||
|
|
||||||
|
// 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],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const snappedGridX = effectiveGridX + snapOffset.x;
|
||||||
|
const snappedGridY = effectiveGridY + snapOffset.y;
|
||||||
|
dxFromLastCommitted = snappedGridX - rx - lastCommittedX;
|
||||||
|
dyFromLastCommitted = snappedGridY - ry - lastCommittedY;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
snapLines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
snapLines: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update last uncommitted point
|
// update last uncommitted point
|
||||||
this.scene.mutateElement(
|
this.scene.mutateElement(
|
||||||
multiElement,
|
multiElement,
|
||||||
@ -8675,7 +8774,10 @@ 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) {
|
const rotateWithDiscreteAngle =
|
||||||
|
shouldRotateWithDiscreteAngle(event) && points.length === 2;
|
||||||
|
|
||||||
|
if (rotateWithDiscreteAngle) {
|
||||||
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
||||||
newElement.x,
|
newElement.x,
|
||||||
newElement.y,
|
newElement.y,
|
||||||
@ -8684,6 +8786,90 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const effectiveGridX = newElement.x + dx;
|
||||||
|
const effectiveGridY = newElement.y + dy;
|
||||||
|
|
||||||
|
// Snap a two-point line/arrow as well
|
||||||
|
if (!isElbowArrow(newElement)) {
|
||||||
|
const { snapOffset, snapLines } = snapLinearElementPoint(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
newElement,
|
||||||
|
points.length - 1,
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
this,
|
||||||
|
event,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
{ includeSelfPoints: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snapLines.length > 0) {
|
||||||
|
if (rotateWithDiscreteAngle) {
|
||||||
|
const angleLine = line<GlobalPoint>(
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
pointFrom(newElement.x, newElement.y),
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstSnapLine = snapLines.find(
|
||||||
|
(snapLine) =>
|
||||||
|
snapLine.type === "points" && snapLine.points.length > 2,
|
||||||
|
);
|
||||||
|
if (firstSnapLine && firstSnapLine.points.length > 1) {
|
||||||
|
const snapLine = line(
|
||||||
|
firstSnapLine.points[0],
|
||||||
|
firstSnapLine.points[1],
|
||||||
|
);
|
||||||
|
const intersection = linesIntersectAt<GlobalPoint>(
|
||||||
|
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],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
firstSnapLine.points = [furthestPoint.point, intersection];
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
snapLines: [firstSnapLine],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
snapLines: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dx = gridX + snapOffset.x - newElement.x;
|
||||||
|
dy = gridY + snapOffset.y - newElement.y;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
snapLines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
snapLines: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (points.length === 1) {
|
if (points.length === 1) {
|
||||||
this.scene.mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
|
@ -11,11 +11,10 @@ import {
|
|||||||
|
|
||||||
import { getShortcutKey } from "@excalidraw/common";
|
import { getShortcutKey } from "@excalidraw/common";
|
||||||
|
|
||||||
import { isNodeInFlowchart } from "@excalidraw/element";
|
import { isNodeInFlowchart, isGridModeEnabled } from "@excalidraw/element";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isEraserActive } from "../appState";
|
import { isEraserActive } from "../appState";
|
||||||
import { isGridModeEnabled } from "../snapping";
|
|
||||||
|
|
||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
|
|
||||||
|
@ -12,10 +12,11 @@ import { frameAndChildrenSelectedTogether } from "@excalidraw/element";
|
|||||||
|
|
||||||
import { elementsAreInSameGroup } from "@excalidraw/element";
|
import { elementsAreInSameGroup } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { isGridModeEnabled } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { isGridModeEnabled } from "../../snapping";
|
|
||||||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||||
import { Island } from "../Island";
|
import { Island } from "../Island";
|
||||||
import { CloseIcon } from "../icons";
|
import { CloseIcon } from "../icons";
|
||||||
|
@ -2,7 +2,8 @@ import { pointFrom, type GlobalPoint, type LocalPoint } from "@excalidraw/math";
|
|||||||
|
|
||||||
import { THEME } from "@excalidraw/common";
|
import { THEME } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { PointSnapLine, PointerSnapLine } from "../snapping";
|
import type { PointSnapLine, PointerSnapLine } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { InteractiveCanvasAppState } from "../types";
|
import type { InteractiveCanvasAppState } from "../types";
|
||||||
|
|
||||||
const SNAP_COLOR_LIGHT = "#ff6b6b";
|
const SNAP_COLOR_LIGHT = "#ff6b6b";
|
||||||
|
@ -8651,7 +8651,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -9294,7 +9297,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
"originSnapOffset": null,
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
|
@ -11,6 +11,8 @@ import type { LinearElementEditor } from "@excalidraw/element";
|
|||||||
|
|
||||||
import type { MaybeTransformHandleType } from "@excalidraw/element";
|
import type { MaybeTransformHandleType } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { SnapLine } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
PointerType,
|
PointerType,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -55,7 +57,6 @@ import type App from "./components/App";
|
|||||||
import type Library from "./data/library";
|
import type Library from "./data/library";
|
||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { ContextMenuItems } from "./components/ContextMenu";
|
import type { ContextMenuItems } from "./components/ContextMenu";
|
||||||
import type { SnapLine } from "./snapping";
|
|
||||||
import type { ImportedDataState } from "./data/types";
|
import type { ImportedDataState } from "./data/types";
|
||||||
|
|
||||||
import type { Language } from "./i18n";
|
import type { Language } from "./i18n";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user