Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e98e2cccb8 | |||
| 8424d78254 | |||
| 8763bebb59 | |||
| 483a225eac | |||
| 79e802d9ed | |||
| 309849925d | |||
| 971237c0df | |||
| 79beed3f5c | |||
| 702e029755 | |||
| 0bbaf34187 | |||
| 18febfeaf2 | |||
| 53557919dd | |||
| 60a459b135 | |||
| 7332e76d56 | |||
| dceaa53b0c | |||
| 6e968324fb | |||
| 09b18cacec | |||
| 0e197ef5c4 | |||
| a0f7edadec | |||
| 58c9bb4712 | |||
| d1c6304d42 | |||
| c1a54455bb | |||
| 07640dd756 | |||
| 5403fa8a0d |
@@ -88,6 +88,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,7 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
|
line,
|
||||||
curveLength,
|
curveLength,
|
||||||
curvePointAtLength,
|
curvePointAtLength,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
@@ -29,6 +30,9 @@ import {
|
|||||||
isPathALoop,
|
isPathALoop,
|
||||||
moveArrowAboveBindable,
|
moveArrowAboveBindable,
|
||||||
projectFixedPointOntoDiagonal,
|
projectFixedPointOntoDiagonal,
|
||||||
|
snapLinearElementPoint,
|
||||||
|
snapToDiscreteAngle,
|
||||||
|
type SnapLine,
|
||||||
type Store,
|
type Store,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
@@ -48,6 +52,7 @@ import {
|
|||||||
calculateFixedPointForNonElbowArrowBinding,
|
calculateFixedPointForNonElbowArrowBinding,
|
||||||
getBindingStrategyForDraggingBindingElementEndpoints,
|
getBindingStrategyForDraggingBindingElementEndpoints,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
|
maxBindingDistance_simple,
|
||||||
snapToMid,
|
snapToMid,
|
||||||
updateBoundPoint,
|
updateBoundPoint,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
@@ -56,6 +61,7 @@ import {
|
|||||||
getElementPointsCoords,
|
getElementPointsCoords,
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
import { getHoveredElementForBinding } from "./collision";
|
||||||
|
|
||||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
@@ -294,7 +300,10 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
): Pick<AppState, "suggestedBinding" | "selectedLinearElement"> | null {
|
): Pick<
|
||||||
|
AppState,
|
||||||
|
"suggestedBinding" | "selectedLinearElement" | "snapLines"
|
||||||
|
> | null {
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const elements = app.scene.getNonDeletedElements();
|
const elements = app.scene.getNonDeletedElements();
|
||||||
const { elementId } = linearElementEditor;
|
const { elementId } = linearElementEditor;
|
||||||
@@ -311,36 +320,26 @@ export class LinearElementEditor {
|
|||||||
linearElementEditor.customLineAngle ??
|
linearElementEditor.customLineAngle ??
|
||||||
determineCustomLinearAngle(pivotPoint, element.points[idx]);
|
determineCustomLinearAngle(pivotPoint, element.points[idx]);
|
||||||
|
|
||||||
// Determine if point movement should happen and how much
|
const { point: newDraggingPointPosition, snapLines } =
|
||||||
let deltaX = 0;
|
LinearElementEditor._getSnappedPointForLinearElement({
|
||||||
let deltaY = 0;
|
app,
|
||||||
if (shouldRotateWithDiscreteAngle(event)) {
|
event,
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
elements,
|
||||||
element,
|
|
||||||
elementsMap,
|
elementsMap,
|
||||||
pivotPoint,
|
element,
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
pointIndex: idx,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
pointerOffset: linearElementEditor.pointerOffset,
|
||||||
|
referencePoint: shouldRotateWithDiscreteAngle(event)
|
||||||
|
? pivotPoint
|
||||||
|
: null,
|
||||||
|
selectedPointsIndices: [idx],
|
||||||
customLineAngle,
|
customLineAngle,
|
||||||
);
|
});
|
||||||
const target = pointFrom<LocalPoint>(
|
|
||||||
width + pivotPoint[0],
|
|
||||||
height + pivotPoint[1],
|
|
||||||
);
|
|
||||||
|
|
||||||
deltaX = target[0] - point[0];
|
const deltaX = newDraggingPointPosition[0] - point[0];
|
||||||
deltaY = target[1] - point[1];
|
const deltaY = newDraggingPointPosition[1] - point[1];
|
||||||
} else {
|
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
|
||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
|
||||||
);
|
|
||||||
deltaX = newDraggingPointPosition[0] - point[0];
|
|
||||||
deltaY = newDraggingPointPosition[1] - point[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the point movement if needed
|
// Apply the point movement if needed
|
||||||
let suggestedBinding: AppState["suggestedBinding"] = null;
|
let suggestedBinding: AppState["suggestedBinding"] = null;
|
||||||
@@ -398,6 +397,8 @@ export class LinearElementEditor {
|
|||||||
// PERF: Avoid state updates if not absolutely necessary
|
// PERF: Avoid state updates if not absolutely necessary
|
||||||
if (
|
if (
|
||||||
app.state.selectedLinearElement?.customLineAngle === customLineAngle &&
|
app.state.selectedLinearElement?.customLineAngle === customLineAngle &&
|
||||||
|
app.state.snapLines.length === 0 &&
|
||||||
|
snapLines.length === 0 &&
|
||||||
linearElementEditor.initialState.altFocusPoint &&
|
linearElementEditor.initialState.altFocusPoint &&
|
||||||
(!suggestedBinding ||
|
(!suggestedBinding ||
|
||||||
isShallowEqual(app.state.suggestedBinding ?? [], suggestedBinding))
|
isShallowEqual(app.state.suggestedBinding ?? [], suggestedBinding))
|
||||||
@@ -436,6 +437,7 @@ export class LinearElementEditor {
|
|||||||
return {
|
return {
|
||||||
selectedLinearElement: newLinearElementEditor,
|
selectedLinearElement: newLinearElementEditor,
|
||||||
suggestedBinding,
|
suggestedBinding,
|
||||||
|
snapLines,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +447,10 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
): Pick<AppState, "suggestedBinding" | "selectedLinearElement"> | null {
|
): Pick<
|
||||||
|
AppState,
|
||||||
|
"suggestedBinding" | "selectedLinearElement" | "snapLines"
|
||||||
|
> | null {
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const elements = app.scene.getNonDeletedElements();
|
const elements = app.scene.getNonDeletedElements();
|
||||||
const { elbowed, elementId, initialState } = linearElementEditor;
|
const { elbowed, elementId, initialState } = linearElementEditor;
|
||||||
@@ -493,7 +498,6 @@ export class LinearElementEditor {
|
|||||||
lastClickedPoint = element.points.length - 1;
|
lastClickedPoint = element.points.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// point that's being dragged (out of all selected points)
|
|
||||||
const draggingPoint = element.points[lastClickedPoint];
|
const draggingPoint = element.points[lastClickedPoint];
|
||||||
// The adjacent point to the one dragged point
|
// The adjacent point to the one dragged point
|
||||||
const pivotPoint =
|
const pivotPoint =
|
||||||
@@ -507,35 +511,27 @@ export class LinearElementEditor {
|
|||||||
element.points.length - 1,
|
element.points.length - 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine if point movement should happen and how much
|
const { point: newDraggingPointPosition, snapLines } =
|
||||||
let deltaX = 0;
|
LinearElementEditor._getSnappedPointForLinearElement({
|
||||||
let deltaY = 0;
|
app,
|
||||||
if (shouldRotateWithDiscreteAngle(event) && singlePointDragged) {
|
event,
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
elements,
|
||||||
element,
|
|
||||||
elementsMap,
|
elementsMap,
|
||||||
pivotPoint,
|
element,
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
pointIndex: lastClickedPoint,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
pointerOffset: linearElementEditor.pointerOffset,
|
||||||
|
referencePoint:
|
||||||
|
shouldRotateWithDiscreteAngle(event) && singlePointDragged
|
||||||
|
? pivotPoint
|
||||||
|
: null,
|
||||||
|
selectedPointsIndices,
|
||||||
customLineAngle,
|
customLineAngle,
|
||||||
);
|
});
|
||||||
const target = pointFrom<LocalPoint>(
|
|
||||||
width + pivotPoint[0],
|
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||||
height + pivotPoint[1],
|
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||||
);
|
|
||||||
deltaX = target[0] - draggingPoint[0];
|
|
||||||
deltaY = target[1] - draggingPoint[1];
|
|
||||||
} else {
|
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
|
||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
|
||||||
);
|
|
||||||
deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
|
||||||
deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the point movement if needed
|
// Apply the point movement if needed
|
||||||
let suggestedBinding: AppState["suggestedBinding"] = null;
|
let suggestedBinding: AppState["suggestedBinding"] = null;
|
||||||
@@ -674,6 +670,7 @@ export class LinearElementEditor {
|
|||||||
return {
|
return {
|
||||||
selectedLinearElement: newLinearElementEditor,
|
selectedLinearElement: newLinearElementEditor,
|
||||||
suggestedBinding,
|
suggestedBinding,
|
||||||
|
snapLines,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,7 +1175,10 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
): LinearElementEditor | null {
|
): {
|
||||||
|
editingLinearElement: LinearElementEditor;
|
||||||
|
snapLines: readonly SnapLine[];
|
||||||
|
} | null {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
if (!appState.selectedLinearElement?.isEditing) {
|
if (!appState.selectedLinearElement?.isEditing) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1187,7 +1187,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.selectedLinearElement;
|
return {
|
||||||
|
editingLinearElement: appState.selectedLinearElement,
|
||||||
|
snapLines: appState.snapLines,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
@@ -1199,36 +1202,37 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
return appState.selectedLinearElement?.lastUncommittedPoint
|
return appState.selectedLinearElement?.lastUncommittedPoint
|
||||||
? {
|
? {
|
||||||
...appState.selectedLinearElement,
|
editingLinearElement: {
|
||||||
lastUncommittedPoint: null,
|
...appState.selectedLinearElement,
|
||||||
|
lastUncommittedPoint: null,
|
||||||
|
},
|
||||||
|
snapLines: [],
|
||||||
}
|
}
|
||||||
: appState.selectedLinearElement;
|
: {
|
||||||
|
editingLinearElement: appState.selectedLinearElement,
|
||||||
|
snapLines: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let newPoint: LocalPoint;
|
const anchor = points[points.length - 2];
|
||||||
|
const elements = app.scene.getNonDeletedElements();
|
||||||
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
|
const { point: newPoint, snapLines } =
|
||||||
const anchor = points[points.length - 2];
|
LinearElementEditor._getSnappedPointForLinearElement({
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
app,
|
||||||
element,
|
event,
|
||||||
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
anchor,
|
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
|
||||||
);
|
|
||||||
|
|
||||||
newPoint = pointFrom(width + anchor[0], height + anchor[1]);
|
|
||||||
} else {
|
|
||||||
newPoint = LinearElementEditor.createPointAt(
|
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
pointIndex: points.length - 1,
|
||||||
scenePointerX - appState.selectedLinearElement.pointerOffset.x,
|
scenePointerX,
|
||||||
scenePointerY - appState.selectedLinearElement.pointerOffset.y,
|
scenePointerY,
|
||||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
pointerOffset: appState.selectedLinearElement.pointerOffset,
|
||||||
? null
|
referencePoint:
|
||||||
: app.getEffectiveGridSize(),
|
shouldRotateWithDiscreteAngle(event) && points.length >= 2
|
||||||
);
|
? anchor
|
||||||
}
|
: null,
|
||||||
|
selectedPointsIndices: [points.length - 1],
|
||||||
|
});
|
||||||
|
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
@@ -1236,7 +1240,7 @@ export class LinearElementEditor {
|
|||||||
app.scene,
|
app.scene,
|
||||||
new Map([
|
new Map([
|
||||||
[
|
[
|
||||||
element.points.length - 1,
|
points.length - 1,
|
||||||
{
|
{
|
||||||
point: newPoint,
|
point: newPoint,
|
||||||
},
|
},
|
||||||
@@ -1246,9 +1250,13 @@ export class LinearElementEditor {
|
|||||||
} else {
|
} else {
|
||||||
LinearElementEditor.addPoints(element, app.scene, [newPoint]);
|
LinearElementEditor.addPoints(element, app.scene, [newPoint]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...appState.selectedLinearElement,
|
editingLinearElement: {
|
||||||
lastUncommittedPoint: element.points[element.points.length - 1],
|
...appState.selectedLinearElement,
|
||||||
|
lastUncommittedPoint: element.points[element.points.length - 1],
|
||||||
|
},
|
||||||
|
snapLines,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1274,18 +1282,53 @@ export class LinearElementEditor {
|
|||||||
static getPointsGlobalCoordinates(
|
static getPointsGlobalCoordinates(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
options: {
|
||||||
|
dragOffset?: { x: number; y: number };
|
||||||
|
excludePointsIndices?: readonly number[];
|
||||||
|
} = {},
|
||||||
): GlobalPoint[] {
|
): GlobalPoint[] {
|
||||||
|
const { dragOffset, excludePointsIndices } = options;
|
||||||
|
|
||||||
|
if (!element.points || element.points.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
return element.points.map((p) => {
|
|
||||||
const { x, y } = element;
|
let elementX = element.x;
|
||||||
return pointRotateRads(
|
let elementY = element.y;
|
||||||
pointFrom(x + p[0], y + p[1]),
|
|
||||||
|
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 p = element.points[i];
|
||||||
|
const globalX = elementX + p[0];
|
||||||
|
const globalY = elementY + p[1];
|
||||||
|
|
||||||
|
const rotated = pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(globalX, globalY),
|
||||||
pointFrom(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
});
|
globalPoints.push(rotated);
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getPointAtIndexGlobalCoordinates(
|
static getPointAtIndexGlobalCoordinates(
|
||||||
@@ -1839,6 +1882,222 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static _getPointPlacementGridSize(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
app: AppClassProperties,
|
||||||
|
event: Pick<KeyboardEvent | PointerEvent, typeof KEYS.CTRL_OR_CMD>,
|
||||||
|
): NullableGridSize {
|
||||||
|
return event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
||||||
|
? null
|
||||||
|
: app.getEffectiveGridSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _shouldSkipExternalSnapForBindableTarget({
|
||||||
|
appState,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
element,
|
||||||
|
pointIndex,
|
||||||
|
scenePoint,
|
||||||
|
selectedPointsIndices,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[];
|
||||||
|
elementsMap: NonDeletedSceneElementsMap;
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>;
|
||||||
|
pointIndex: number;
|
||||||
|
scenePoint: GlobalPoint;
|
||||||
|
selectedPointsIndices?: readonly number[];
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
isElbowArrow(element) ||
|
||||||
|
!isBindingElement(element) ||
|
||||||
|
!isBindingEnabled(appState) ||
|
||||||
|
selectedPointsIndices?.length !== 1
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointIndex !== 0 && pointIndex !== element.points.length - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!getHoveredElementForBinding(
|
||||||
|
scenePoint,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
maxBindingDistance_simple(appState.zoom),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _getSnappedPointForLinearElement({
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
element,
|
||||||
|
pointIndex,
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
pointerOffset,
|
||||||
|
referencePoint,
|
||||||
|
selectedPointsIndices,
|
||||||
|
customLineAngle,
|
||||||
|
}: {
|
||||||
|
app: AppClassProperties;
|
||||||
|
event: PointerEvent | React.PointerEvent<HTMLCanvasElement>;
|
||||||
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[];
|
||||||
|
elementsMap: NonDeletedSceneElementsMap;
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>;
|
||||||
|
pointIndex: number;
|
||||||
|
scenePointerX: number;
|
||||||
|
scenePointerY: number;
|
||||||
|
pointerOffset: Readonly<{ x: number; y: number }>;
|
||||||
|
referencePoint?: LocalPoint | null;
|
||||||
|
selectedPointsIndices?: readonly number[];
|
||||||
|
customLineAngle?: number | null;
|
||||||
|
}): {
|
||||||
|
point: LocalPoint;
|
||||||
|
snapLines: SnapLine[];
|
||||||
|
} {
|
||||||
|
const gridSize = LinearElementEditor._getPointPlacementGridSize(
|
||||||
|
element,
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (referencePoint) {
|
||||||
|
const referencePointCoords =
|
||||||
|
LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
referencePoint,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
let { width: dxFromReference, height: dyFromReference } =
|
||||||
|
getLockedLinearCursorAlignSize(
|
||||||
|
referencePointCoords[0],
|
||||||
|
referencePointCoords[1],
|
||||||
|
gridX,
|
||||||
|
gridY,
|
||||||
|
customLineAngle ?? undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const effectiveGridX = referencePointCoords[0] + dxFromReference;
|
||||||
|
const effectiveGridY = referencePointCoords[1] + dyFromReference;
|
||||||
|
|
||||||
|
let snapLines: SnapLine[] = [];
|
||||||
|
const shouldSkipExternalSnap =
|
||||||
|
LinearElementEditor._shouldSkipExternalSnapForBindableTarget({
|
||||||
|
appState: app.state,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
element,
|
||||||
|
pointIndex,
|
||||||
|
scenePoint: pointFrom<GlobalPoint>(effectiveGridX, effectiveGridY),
|
||||||
|
selectedPointsIndices,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isElbowArrow(element)) {
|
||||||
|
const { snapOffset, snapLines: nextSnapLines } = snapLinearElementPoint(
|
||||||
|
elements,
|
||||||
|
element,
|
||||||
|
pointFrom<GlobalPoint>(effectiveGridX, effectiveGridY),
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
elementsMap,
|
||||||
|
{
|
||||||
|
includeExternalPoints: !shouldSkipExternalSnap,
|
||||||
|
includeSelfPoints: true,
|
||||||
|
selectedPointsIndices,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
snapLines = nextSnapLines;
|
||||||
|
|
||||||
|
if (nextSnapLines.length > 0) {
|
||||||
|
const result = snapToDiscreteAngle(
|
||||||
|
nextSnapLines,
|
||||||
|
line(
|
||||||
|
pointFrom(effectiveGridX, effectiveGridY),
|
||||||
|
pointFrom(referencePointCoords[0], referencePointCoords[1]),
|
||||||
|
),
|
||||||
|
pointFrom(gridX, gridY),
|
||||||
|
referencePointCoords,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.snapLines.length > 0) {
|
||||||
|
dxFromReference = result.dxFromReference;
|
||||||
|
dyFromReference = result.dyFromReference;
|
||||||
|
snapLines = result.snapLines;
|
||||||
|
} else {
|
||||||
|
dxFromReference =
|
||||||
|
effectiveGridX + snapOffset.x - referencePointCoords[0];
|
||||||
|
dyFromReference =
|
||||||
|
effectiveGridY + snapOffset.y - referencePointCoords[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rotatedX, rotatedY] = pointRotateRads(
|
||||||
|
pointFrom(dxFromReference, dyFromReference),
|
||||||
|
pointFrom(0, 0),
|
||||||
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
point: pointFrom(
|
||||||
|
referencePoint[0] + rotatedX,
|
||||||
|
referencePoint[1] + rotatedY,
|
||||||
|
),
|
||||||
|
snapLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalPointerX = scenePointerX - pointerOffset.x;
|
||||||
|
const originalPointerY = scenePointerY - pointerOffset.y;
|
||||||
|
const shouldSkipExternalSnap =
|
||||||
|
LinearElementEditor._shouldSkipExternalSnapForBindableTarget({
|
||||||
|
appState: app.state,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
element,
|
||||||
|
pointIndex,
|
||||||
|
scenePoint: pointFrom<GlobalPoint>(originalPointerX, originalPointerY),
|
||||||
|
selectedPointsIndices,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { snapOffset, snapLines } = snapLinearElementPoint(
|
||||||
|
elements,
|
||||||
|
element,
|
||||||
|
pointFrom(originalPointerX, originalPointerY),
|
||||||
|
app,
|
||||||
|
event,
|
||||||
|
elementsMap,
|
||||||
|
{
|
||||||
|
includeExternalPoints: !shouldSkipExternalSnap,
|
||||||
|
includeSelfPoints: true,
|
||||||
|
selectedPointsIndices,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
point: LinearElementEditor.createPointAt(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
originalPointerX + snapOffset.x,
|
||||||
|
originalPointerY + snapOffset.y,
|
||||||
|
gridSize,
|
||||||
|
),
|
||||||
|
snapLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static getBoundTextElementPosition = (
|
static getBoundTextElementPosition = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
isCloseTo,
|
||||||
|
line,
|
||||||
|
linesIntersectAt,
|
||||||
|
pointDistance,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
rangeInclusive,
|
rangeInclusive,
|
||||||
@@ -13,7 +17,7 @@ import {
|
|||||||
getDraggedElementsBounds,
|
getDraggedElementsBounds,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
import { isBoundToContainer } from "@excalidraw/element";
|
import { isBoundToContainer, isElbowArrow } from "@excalidraw/element";
|
||||||
|
|
||||||
import { getMaximumGroups } from "@excalidraw/element";
|
import { getMaximumGroups } from "@excalidraw/element";
|
||||||
|
|
||||||
@@ -29,14 +33,18 @@ import type { MaybeTransformHandleType } from "@excalidraw/element";
|
|||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
NonDeleted,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppState,
|
AppState,
|
||||||
KeyboardModifiersObject,
|
KeyboardModifiersObject,
|
||||||
} from "./types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
|
||||||
const SNAP_DISTANCE = 8;
|
const SNAP_DISTANCE = 8;
|
||||||
|
|
||||||
@@ -122,6 +130,11 @@ export type SnapLine = PointSnapLine | GapSnapLine | PointerSnapLine;
|
|||||||
export class SnapCache {
|
export class SnapCache {
|
||||||
private static referenceSnapPoints: GlobalPoint[] | null = null;
|
private static referenceSnapPoints: GlobalPoint[] | null = null;
|
||||||
|
|
||||||
|
private static linearElementAxisSnapTargets: {
|
||||||
|
editingElementId: ExcalidrawElement["id"];
|
||||||
|
snapTargets: GlobalPoint[];
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
private static visibleGaps: {
|
private static visibleGaps: {
|
||||||
verticalGaps: Gap[];
|
verticalGaps: Gap[];
|
||||||
horizontalGaps: Gap[];
|
horizontalGaps: Gap[];
|
||||||
@@ -135,6 +148,27 @@ export class SnapCache {
|
|||||||
return SnapCache.referenceSnapPoints;
|
return SnapCache.referenceSnapPoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static setLinearElementAxisSnapTargets = (
|
||||||
|
editingElementId: ExcalidrawElement["id"],
|
||||||
|
snapTargets: GlobalPoint[] | null,
|
||||||
|
) => {
|
||||||
|
SnapCache.linearElementAxisSnapTargets = snapTargets
|
||||||
|
? {
|
||||||
|
editingElementId,
|
||||||
|
snapTargets,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getLinearElementAxisSnapTargets = (
|
||||||
|
editingElementId: ExcalidrawElement["id"],
|
||||||
|
) => {
|
||||||
|
return SnapCache.linearElementAxisSnapTargets?.editingElementId ===
|
||||||
|
editingElementId
|
||||||
|
? SnapCache.linearElementAxisSnapTargets.snapTargets
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
public static setVisibleGaps = (
|
public static setVisibleGaps = (
|
||||||
gaps: {
|
gaps: {
|
||||||
verticalGaps: Gap[];
|
verticalGaps: Gap[];
|
||||||
@@ -150,6 +184,7 @@ export class SnapCache {
|
|||||||
|
|
||||||
public static destroy = () => {
|
public static destroy = () => {
|
||||||
SnapCache.referenceSnapPoints = null;
|
SnapCache.referenceSnapPoints = null;
|
||||||
|
SnapCache.linearElementAxisSnapTargets = null;
|
||||||
SnapCache.visibleGaps = null;
|
SnapCache.visibleGaps = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -235,6 +270,19 @@ 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 = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
|
element as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
elementsMap,
|
||||||
|
{
|
||||||
|
dragOffset,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
result = linearPoints;
|
||||||
|
} else if (
|
||||||
(element.type === "diamond" || element.type === "ellipse") &&
|
(element.type === "diamond" || element.type === "ellipse") &&
|
||||||
!boundingBoxCorners
|
!boundingBoxCorners
|
||||||
) {
|
) {
|
||||||
@@ -633,6 +681,227 @@ export const getReferenceSnapPoints = (
|
|||||||
.flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
|
.flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getExternalAxisSnapTargets = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
editingElement: ExcalidrawLinearElement,
|
||||||
|
appState: AppState,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
const cachedAxisSnapTargets = SnapCache.getLinearElementAxisSnapTargets(
|
||||||
|
editingElement.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const externalAxisSnapTargets =
|
||||||
|
cachedAxisSnapTargets ??
|
||||||
|
getReferenceSnapPoints(elements, [editingElement], appState, elementsMap);
|
||||||
|
|
||||||
|
if (!cachedAxisSnapTargets) {
|
||||||
|
SnapCache.setLinearElementAxisSnapTargets(
|
||||||
|
editingElement.id,
|
||||||
|
externalAxisSnapTargets,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return externalAxisSnapTargets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOwnAxisSnapTargets = (
|
||||||
|
editingElement: ExcalidrawLinearElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
selectedPointsIndices?: readonly number[],
|
||||||
|
) => {
|
||||||
|
return LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
|
editingElement as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
elementsMap,
|
||||||
|
{
|
||||||
|
excludePointsIndices: selectedPointsIndices,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAxisSnapTargets = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
editingElement: ExcalidrawLinearElement,
|
||||||
|
appState: AppState,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
options: {
|
||||||
|
includeSelfPoints?: boolean;
|
||||||
|
selectedPointsIndices?: readonly number[];
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
const externalAxisSnapTargets = getExternalAxisSnapTargets(
|
||||||
|
elements,
|
||||||
|
editingElement,
|
||||||
|
appState,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options.includeSelfPoints) {
|
||||||
|
return externalAxisSnapTargets;
|
||||||
|
}
|
||||||
|
|
||||||
|
return externalAxisSnapTargets.concat(
|
||||||
|
getOwnAxisSnapTargets(
|
||||||
|
editingElement,
|
||||||
|
elementsMap,
|
||||||
|
options.selectedPointsIndices,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectNearestAxisSnapCandidates = (
|
||||||
|
axisSnapTargets: readonly GlobalPoint[],
|
||||||
|
pointerPosition: GlobalPoint,
|
||||||
|
nearestSnapsX: Snaps,
|
||||||
|
nearestSnapsY: Snaps,
|
||||||
|
minOffset: Vector2D,
|
||||||
|
) => {
|
||||||
|
for (const snapTarget of axisSnapTargets) {
|
||||||
|
const offsetX = snapTarget[0] - pointerPosition[0];
|
||||||
|
const offsetY = snapTarget[1] - pointerPosition[1];
|
||||||
|
const absOffsetX = Math.abs(offsetX);
|
||||||
|
const absOffsetY = Math.abs(offsetY);
|
||||||
|
|
||||||
|
if (absOffsetX > minOffset.x && absOffsetY > minOffset.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absOffsetX <= minOffset.x) {
|
||||||
|
if (absOffsetX < minOffset.x) {
|
||||||
|
nearestSnapsX.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestSnapsX.push({
|
||||||
|
type: "point",
|
||||||
|
points: [pointerPosition, snapTarget],
|
||||||
|
offset: offsetX,
|
||||||
|
});
|
||||||
|
|
||||||
|
minOffset.x = absOffsetX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absOffsetY <= minOffset.y) {
|
||||||
|
if (absOffsetY < minOffset.y) {
|
||||||
|
nearestSnapsY.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestSnapsY.push({
|
||||||
|
type: "point",
|
||||||
|
points: [pointerPosition, snapTarget],
|
||||||
|
offset: offsetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
minOffset.y = absOffsetY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const snapLinearElementPoint = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
editingElement: ExcalidrawLinearElement,
|
||||||
|
pointerPosition: GlobalPoint,
|
||||||
|
app: AppClassProperties,
|
||||||
|
event: KeyboardModifiersObject,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
options: {
|
||||||
|
includeExternalPoints?: boolean;
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
if (options.includeExternalPoints !== false) {
|
||||||
|
collectNearestAxisSnapCandidates(
|
||||||
|
getExternalAxisSnapTargets(
|
||||||
|
elements,
|
||||||
|
editingElement,
|
||||||
|
app.state,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
pointerPosition,
|
||||||
|
nearestSnapsX,
|
||||||
|
nearestSnapsY,
|
||||||
|
minOffset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeSelfPoints) {
|
||||||
|
collectNearestAxisSnapCandidates(
|
||||||
|
getOwnAxisSnapTargets(
|
||||||
|
editingElement,
|
||||||
|
elementsMap,
|
||||||
|
options.selectedPointsIndices,
|
||||||
|
),
|
||||||
|
pointerPosition,
|
||||||
|
nearestSnapsX,
|
||||||
|
nearestSnapsY,
|
||||||
|
minOffset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const snappedPosition = pointFrom<GlobalPoint>(
|
||||||
|
pointerPosition[0] + snapOffset.x,
|
||||||
|
pointerPosition[1] + snapOffset.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
const snappedSnapsX = nearestSnapsX
|
||||||
|
.filter(
|
||||||
|
(snap): snap is PointSnap =>
|
||||||
|
snap.type === "point" && isCloseTo(snap.offset, snapOffset.x, 0.01),
|
||||||
|
)
|
||||||
|
.map((snap) => ({
|
||||||
|
type: "point" as const,
|
||||||
|
points: [snappedPosition, snap.points[1]] as [GlobalPoint, GlobalPoint],
|
||||||
|
offset: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const snappedSnapsY = nearestSnapsY
|
||||||
|
.filter(
|
||||||
|
(snap): snap is PointSnap =>
|
||||||
|
snap.type === "point" && isCloseTo(snap.offset, snapOffset.y, 0.01),
|
||||||
|
)
|
||||||
|
.map((snap) => ({
|
||||||
|
type: "point" as const,
|
||||||
|
points: [snappedPosition, snap.points[1]] as [GlobalPoint, GlobalPoint],
|
||||||
|
offset: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
pointSnapLines = createPointSnapLines(snappedSnapsX, snappedSnapsY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
snapOffset,
|
||||||
|
snapLines: pointSnapLines,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getPointSnaps = (
|
const getPointSnaps = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
selectionSnapPoints: GlobalPoint[],
|
selectionSnapPoints: GlobalPoint[],
|
||||||
@@ -1412,3 +1681,79 @@ export const isActiveToolNonLinearSnappable = (
|
|||||||
activeToolType === TOOL_TYPE.text
|
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<GlobalPoint>(
|
||||||
|
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],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -155,6 +155,24 @@ describe("Test Linear Elements", () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dragMove = (startPoint: GlobalPoint, endPoint: GlobalPoint) => {
|
||||||
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
|
clientX: startPoint[0],
|
||||||
|
clientY: startPoint[1],
|
||||||
|
});
|
||||||
|
fireEvent.pointerMove(interactiveCanvas, {
|
||||||
|
clientX: endPoint[0],
|
||||||
|
clientY: endPoint[1],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragEnd = (endPoint: GlobalPoint) => {
|
||||||
|
fireEvent.pointerUp(interactiveCanvas, {
|
||||||
|
clientX: endPoint[0],
|
||||||
|
clientY: endPoint[1],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const deletePoint = (point: GlobalPoint) => {
|
const deletePoint = (point: GlobalPoint) => {
|
||||||
fireEvent.pointerDown(interactiveCanvas, {
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
clientX: point[0],
|
clientX: point[0],
|
||||||
@@ -258,6 +276,73 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
|
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows snap lines and snaps the endpoint when creating a line", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
});
|
||||||
|
API.setElements([rect]);
|
||||||
|
API.setAppState({ objectsSnapModeEnabled: true });
|
||||||
|
|
||||||
|
UI.clickTool("line");
|
||||||
|
|
||||||
|
const startPoint = pointFrom<GlobalPoint>(20, 20);
|
||||||
|
const pointerNearCorner = pointFrom<GlobalPoint>(95, 95);
|
||||||
|
|
||||||
|
dragMove(startPoint, pointerNearCorner);
|
||||||
|
|
||||||
|
expect(h.state.snapLines.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
dragEnd(pointerNearCorner);
|
||||||
|
|
||||||
|
const line = h.elements.find(
|
||||||
|
(element): element is ExcalidrawLinearElement => element.type === "line",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(line).toBeDefined();
|
||||||
|
|
||||||
|
const endpoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
line!,
|
||||||
|
line!.points[line!.points.length - 1],
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoint).toEqual(pointFrom<GlobalPoint>(100, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers binding over external snaps when creating an arrow endpoint", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
});
|
||||||
|
API.setElements([rect]);
|
||||||
|
API.setAppState({ objectsSnapModeEnabled: true });
|
||||||
|
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
|
||||||
|
const startPoint = pointFrom<GlobalPoint>(20, 20);
|
||||||
|
const pointerNearBindable = pointFrom<GlobalPoint>(96, 118);
|
||||||
|
|
||||||
|
dragMove(startPoint, pointerNearBindable);
|
||||||
|
|
||||||
|
expect(h.state.suggestedBinding?.element.id).toBe(rect.id);
|
||||||
|
expect(h.state.snapLines).toEqual([]);
|
||||||
|
|
||||||
|
dragEnd(pointerNearBindable);
|
||||||
|
|
||||||
|
const arrow = h.elements.find(
|
||||||
|
(element): element is ExcalidrawLinearElement => element.type === "arrow",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(arrow?.endBinding?.elementId).toBe(rect.id);
|
||||||
|
});
|
||||||
|
|
||||||
it("should enter line editor via enter (line)", () => {
|
it("should enter line editor via enter (line)", () => {
|
||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
@@ -401,6 +486,77 @@ describe("Test Linear Elements", () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows snap lines when dragging a point to another line point axis", () => {
|
||||||
|
const line = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
roughness: 0,
|
||||||
|
points: [
|
||||||
|
pointFrom<LocalPoint>(0, 0),
|
||||||
|
pointFrom<LocalPoint>(50, 50),
|
||||||
|
pointFrom<LocalPoint>(100, 0),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([line]);
|
||||||
|
API.setAppState({ objectsSnapModeEnabled: true });
|
||||||
|
enterLineEditingMode(line);
|
||||||
|
|
||||||
|
const middlePoint = pointFrom<GlobalPoint>(70, 70);
|
||||||
|
const pointerNearEndPointX = pointFrom<GlobalPoint>(117, 65);
|
||||||
|
|
||||||
|
dragMove(middlePoint, pointerNearEndPointX);
|
||||||
|
|
||||||
|
expect(h.state.snapLines.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
dragEnd(pointerNearEndPointX);
|
||||||
|
|
||||||
|
expect(API.getElement(line).points[1]).toEqual(
|
||||||
|
pointFrom<LocalPoint>(100, 45),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers binding over external snaps when dragging an existing arrow endpoint", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
width: 40,
|
||||||
|
height: 0,
|
||||||
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(40, 0)],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
API.setAppState({ objectsSnapModeEnabled: true });
|
||||||
|
enterLineEditingMode(arrow);
|
||||||
|
|
||||||
|
const endPoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||||
|
arrow,
|
||||||
|
arrow.points[arrow.points.length - 1],
|
||||||
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
const pointerNearBindable = pointFrom<GlobalPoint>(96, 118);
|
||||||
|
|
||||||
|
dragMove(endPoint, pointerNearBindable);
|
||||||
|
|
||||||
|
expect(h.state.suggestedBinding?.element.id).toBe(rect.id);
|
||||||
|
expect(h.state.snapLines).toEqual([]);
|
||||||
|
|
||||||
|
dragEnd(pointerNearBindable);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).endBinding?.elementId).toBe(rect.id);
|
||||||
|
});
|
||||||
|
|
||||||
it("should update the midpoints when element roundness changed", async () => {
|
it("should update the midpoints when element roundness changed", async () => {
|
||||||
createThreePointerLinearElement("line");
|
createThreePointerLinearElement("line");
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,16 @@ import {
|
|||||||
hitElementBoundingBox,
|
hitElementBoundingBox,
|
||||||
isLineElement,
|
isLineElement,
|
||||||
isSimpleArrow,
|
isSimpleArrow,
|
||||||
|
isGridModeEnabled,
|
||||||
|
SnapCache,
|
||||||
|
isActiveToolNonLinearSnappable,
|
||||||
|
getSnapLinesAtPointer,
|
||||||
|
isSnappingEnabled,
|
||||||
|
getReferenceSnapPoints,
|
||||||
|
getVisibleGaps,
|
||||||
|
snapDraggedElements,
|
||||||
|
snapNewElement,
|
||||||
|
snapResizingElements,
|
||||||
StoreDelta,
|
StoreDelta,
|
||||||
type ApplyToOptions,
|
type ApplyToOptions,
|
||||||
positionElementsOnGrid,
|
positionElementsOnGrid,
|
||||||
@@ -396,18 +406,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 { Renderer } from "../scene/Renderer";
|
import { Renderer } from "../scene/Renderer";
|
||||||
import {
|
import {
|
||||||
setEraserCursor,
|
setEraserCursor,
|
||||||
@@ -785,6 +783,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private withStableSnapLines<T extends { snapLines: AppState["snapLines"] }>(
|
||||||
|
state: T,
|
||||||
|
): T {
|
||||||
|
const snapLines = updateStable(this.state.snapLines, state.snapLines);
|
||||||
|
|
||||||
|
return snapLines === state.snapLines
|
||||||
|
? state
|
||||||
|
: {
|
||||||
|
...state,
|
||||||
|
snapLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldUpdateSelectedLinearElementState(
|
||||||
|
selectedLinearElement: AppState["selectedLinearElement"],
|
||||||
|
snapLines: AppState["snapLines"],
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
selectedLinearElement !== this.state.selectedLinearElement ||
|
||||||
|
snapLines !== this.state.snapLines
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
@@ -6786,7 +6807,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
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(),
|
||||||
@@ -6835,7 +6859,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.selectedLinearElement?.isEditing &&
|
this.state.selectedLinearElement?.isEditing &&
|
||||||
!this.state.selectedLinearElement.isDragging
|
!this.state.selectedLinearElement.isDragging
|
||||||
) {
|
) {
|
||||||
const editingLinearElement = this.state.newElement
|
const result = this.state.newElement
|
||||||
? null
|
? null
|
||||||
: LinearElementEditor.handlePointerMoveInEditMode(
|
: LinearElementEditor.handlePointerMoveInEditMode(
|
||||||
event,
|
event,
|
||||||
@@ -6844,18 +6868,33 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (result) {
|
||||||
editingLinearElement &&
|
const { editingLinearElement, snapLines } = result;
|
||||||
editingLinearElement !== this.state.selectedLinearElement
|
const nextState = this.withStableSnapLines({
|
||||||
) {
|
selectedLinearElement: editingLinearElement,
|
||||||
// Since we are reading from previous state which is not possible with
|
snapLines,
|
||||||
// automatic batching in React 18 hence using flush sync to synchronously
|
|
||||||
// update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details.
|
|
||||||
flushSync(() => {
|
|
||||||
this.setState({
|
|
||||||
selectedLinearElement: editingLinearElement,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
editingLinearElement &&
|
||||||
|
this.shouldUpdateSelectedLinearElementState(
|
||||||
|
nextState.selectedLinearElement,
|
||||||
|
nextState.snapLines,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Since we are reading from previous state which is not possible with
|
||||||
|
// automatic batching in React 18 hence using flush sync to synchronously
|
||||||
|
// update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details.
|
||||||
|
flushSync(() => {
|
||||||
|
this.setState(nextState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
editingLinearElement.lastUncommittedPoint == null &&
|
||||||
|
this.state.suggestedBinding
|
||||||
|
) {
|
||||||
|
this.setState({ suggestedBinding: null });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9713,25 +9752,27 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
|
const nextState = this.withStableSnapLines(newState);
|
||||||
|
|
||||||
// NOTE: Optimize setState calls because it
|
// NOTE: Optimize setState calls because it
|
||||||
// affects history and performance
|
// affects history and performance
|
||||||
if (
|
if (
|
||||||
newState.suggestedBinding !== this.state.suggestedBinding ||
|
nextState.suggestedBinding !== this.state.suggestedBinding ||
|
||||||
!isShallowEqual(
|
!isShallowEqual(
|
||||||
newState.selectedLinearElement?.selectedPointsIndices ?? [],
|
nextState.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
) ||
|
) ||
|
||||||
newState.selectedLinearElement?.hoverPointIndex !==
|
nextState.selectedLinearElement?.hoverPointIndex !==
|
||||||
this.state.selectedLinearElement?.hoverPointIndex ||
|
this.state.selectedLinearElement?.hoverPointIndex ||
|
||||||
newState.selectedLinearElement?.customLineAngle !==
|
nextState.selectedLinearElement?.customLineAngle !==
|
||||||
this.state.selectedLinearElement?.customLineAngle ||
|
this.state.selectedLinearElement?.customLineAngle ||
|
||||||
this.state.selectedLinearElement.isDragging !==
|
this.state.selectedLinearElement.isDragging !==
|
||||||
newState.selectedLinearElement?.isDragging ||
|
nextState.selectedLinearElement?.isDragging ||
|
||||||
this.state.selectedLinearElement?.initialState?.altFocusPoint !==
|
this.state.selectedLinearElement?.initialState?.altFocusPoint !==
|
||||||
newState.selectedLinearElement?.initialState?.altFocusPoint
|
nextState.selectedLinearElement?.initialState?.altFocusPoint ||
|
||||||
|
nextState.snapLines !== this.state.snapLines
|
||||||
) {
|
) {
|
||||||
this.setState(newState);
|
this.setState(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -10420,8 +10461,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.lassoTrail.endPath();
|
this.lassoTrail.endPath();
|
||||||
this.previousPointerMoveCoords = null;
|
this.previousPointerMoveCoords = null;
|
||||||
|
|
||||||
SnapCache.setReferenceSnapPoints(null);
|
SnapCache.destroy();
|
||||||
SnapCache.setVisibleGaps(null);
|
|
||||||
|
|
||||||
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "@excalidraw/common";
|
|||||||
import {
|
import {
|
||||||
isFlowchartNodeElement,
|
isFlowchartNodeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
|
isGridModeEnabled,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isLineElement,
|
isLineElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
@@ -16,7 +17,6 @@ import type { EditorInterface } from "@excalidraw/common";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getShortcutKey } from "../shortcut";
|
import { getShortcutKey } from "../shortcut";
|
||||||
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";
|
||||||
|
|||||||
@@ -8665,7 +8665,14 @@ 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": {
|
||||||
|
"data": null,
|
||||||
|
"shown": false,
|
||||||
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
@@ -9322,7 +9329,14 @@ 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": {
|
||||||
|
"data": null,
|
||||||
|
"shown": false,
|
||||||
|
},
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"preferredSelectionTool": {
|
"preferredSelectionTool": {
|
||||||
|
|||||||
@@ -10,6 +10,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 { ClipboardData } from "./clipboard";
|
|||||||
import type App from "./components/App";
|
import type App from "./components/App";
|
||||||
import type Library from "./data/library";
|
import type Library from "./data/library";
|
||||||
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";
|
||||||
|
|||||||
Reference in New Issue
Block a user