fix(line-snap): prefer bindable targets over external snaps
This commit is contained in:
@@ -52,6 +52,7 @@ import {
|
|||||||
calculateFixedPointForNonElbowArrowBinding,
|
calculateFixedPointForNonElbowArrowBinding,
|
||||||
getBindingStrategyForDraggingBindingElementEndpoints,
|
getBindingStrategyForDraggingBindingElementEndpoints,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
|
maxBindingDistance_simple,
|
||||||
snapToMid,
|
snapToMid,
|
||||||
updateBoundPoint,
|
updateBoundPoint,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
@@ -60,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";
|
||||||
@@ -1890,6 +1892,44 @@ export class LinearElementEditor {
|
|||||||
: app.getEffectiveGridSize();
|
: 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({
|
private static _getSnappedPointForLinearElement({
|
||||||
app,
|
app,
|
||||||
event,
|
event,
|
||||||
@@ -1952,6 +1992,16 @@ export class LinearElementEditor {
|
|||||||
const effectiveGridY = referencePointCoords[1] + dyFromReference;
|
const effectiveGridY = referencePointCoords[1] + dyFromReference;
|
||||||
|
|
||||||
let snapLines: SnapLine[] = [];
|
let snapLines: SnapLine[] = [];
|
||||||
|
const shouldSkipExternalSnap =
|
||||||
|
LinearElementEditor._shouldSkipExternalSnapForBindableTarget({
|
||||||
|
appState: app.state,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
element,
|
||||||
|
pointIndex,
|
||||||
|
scenePoint: pointFrom<GlobalPoint>(effectiveGridX, effectiveGridY),
|
||||||
|
selectedPointsIndices,
|
||||||
|
});
|
||||||
|
|
||||||
if (!isElbowArrow(element)) {
|
if (!isElbowArrow(element)) {
|
||||||
const { snapOffset, snapLines: nextSnapLines } = snapLinearElementPoint(
|
const { snapOffset, snapLines: nextSnapLines } = snapLinearElementPoint(
|
||||||
@@ -1962,6 +2012,7 @@ export class LinearElementEditor {
|
|||||||
event,
|
event,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
{
|
{
|
||||||
|
includeExternalPoints: !shouldSkipExternalSnap,
|
||||||
includeSelfPoints: true,
|
includeSelfPoints: true,
|
||||||
selectedPointsIndices,
|
selectedPointsIndices,
|
||||||
},
|
},
|
||||||
@@ -2010,6 +2061,16 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
const originalPointerX = scenePointerX - pointerOffset.x;
|
const originalPointerX = scenePointerX - pointerOffset.x;
|
||||||
const originalPointerY = scenePointerY - pointerOffset.y;
|
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(
|
const { snapOffset, snapLines } = snapLinearElementPoint(
|
||||||
elements,
|
elements,
|
||||||
@@ -2019,6 +2080,7 @@ export class LinearElementEditor {
|
|||||||
event,
|
event,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
{
|
{
|
||||||
|
includeExternalPoints: !shouldSkipExternalSnap,
|
||||||
includeSelfPoints: true,
|
includeSelfPoints: true,
|
||||||
selectedPointsIndices,
|
selectedPointsIndices,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -804,6 +804,7 @@ export const snapLinearElementPoint = (
|
|||||||
event: KeyboardModifiersObject,
|
event: KeyboardModifiersObject,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
options: {
|
options: {
|
||||||
|
includeExternalPoints?: boolean;
|
||||||
includeSelfPoints?: boolean;
|
includeSelfPoints?: boolean;
|
||||||
selectedPointsIndices?: readonly number[];
|
selectedPointsIndices?: readonly number[];
|
||||||
} = {},
|
} = {},
|
||||||
@@ -827,6 +828,7 @@ export const snapLinearElementPoint = (
|
|||||||
const nearestSnapsX: Snaps = [];
|
const nearestSnapsX: Snaps = [];
|
||||||
const nearestSnapsY: Snaps = [];
|
const nearestSnapsY: Snaps = [];
|
||||||
|
|
||||||
|
if (options.includeExternalPoints !== false) {
|
||||||
collectNearestAxisSnapCandidates(
|
collectNearestAxisSnapCandidates(
|
||||||
getExternalAxisSnapTargets(
|
getExternalAxisSnapTargets(
|
||||||
elements,
|
elements,
|
||||||
@@ -839,6 +841,7 @@ export const snapLinearElementPoint = (
|
|||||||
nearestSnapsY,
|
nearestSnapsY,
|
||||||
minOffset,
|
minOffset,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (options.includeSelfPoints) {
|
if (options.includeSelfPoints) {
|
||||||
collectNearestAxisSnapCandidates(
|
collectNearestAxisSnapCandidates(
|
||||||
|
|||||||
@@ -313,6 +313,36 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(endpoint).toEqual(pointFrom<GlobalPoint>(100, 100));
|
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);
|
||||||
@@ -489,6 +519,44 @@ describe("Test Linear Elements", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user