fix: Centralized midpoint snap code

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2026-05-08 13:58:30 +00:00
parent 4e94b02375
commit 4a161c1764
15 changed files with 313 additions and 230 deletions
+1
View File
@@ -269,6 +269,7 @@ export const handleFocusPointDrag = (
newMode || "orbit",
linearElementEditor.draggedFocusPointBinding,
scene,
appState.zoom,
point,
);
}
+18 -116
View File
@@ -60,6 +60,7 @@ import { updateElbowArrowPoints } from "./elbowArrow";
import {
deconstructDiamondElement,
deconstructRectanguloidElement,
getSnapOutlineMidPoint,
projectFixedPointOntoDiagonal,
} from "./utils";
@@ -175,6 +176,7 @@ export const bindOrUnbindBindingElement = (
start,
"start",
scene,
appState.zoom,
appState.isBindingEnabled,
);
bindOrUnbindBindingElementEdge(
@@ -182,6 +184,7 @@ export const bindOrUnbindBindingElement = (
end,
"end",
scene,
appState.zoom,
appState.isBindingEnabled,
);
if (start.focusPoint || end.focusPoint) {
@@ -226,6 +229,7 @@ const bindOrUnbindBindingElementEdge = (
{ mode, element, focusPoint }: BindingStrategy,
startOrEnd: "start" | "end",
scene: Scene,
zoom: AppState["zoom"],
shouldSnapToOutline = true,
): void => {
if (mode === null) {
@@ -238,6 +242,7 @@ const bindOrUnbindBindingElementEdge = (
mode,
startOrEnd,
scene,
zoom,
focusPoint,
shouldSnapToOutline,
);
@@ -1026,6 +1031,7 @@ export const bindBindingElement = (
mode: BindMode,
startOrEnd: "start" | "end",
scene: Scene,
zoom: AppState["zoom"],
focusPoint?: GlobalPoint,
shouldSnapToOutline = true,
): void => {
@@ -1042,6 +1048,7 @@ export const bindBindingElement = (
hoveredElement,
startOrEnd,
elementsMap,
zoom,
shouldSnapToOutline,
),
};
@@ -1266,6 +1273,7 @@ const updateArrowBindings = (
strategy[strategyName].mode,
strategyName,
scene,
appState.zoom,
strategy[strategyName].focusPoint,
);
}
@@ -1375,6 +1383,7 @@ export const bindPointToSnapToElementOutline = (
bindableElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end",
elementsMap: ElementsMap,
zoom: AppState["zoom"],
customIntersector?: LineSegment<GlobalPoint>,
isMidpointSnappingEnabled = true,
): GlobalPoint => {
@@ -1417,7 +1426,13 @@ export const bindPointToSnapToElementOutline = (
headingForPointFromElement(bindableElement, aabb, point),
);
const snapPoint = isMidpointSnappingEnabled
? snapToMid(bindableElement, elementsMap, edgePoint, 0.05, arrowElement)
? getSnapOutlineMidPoint(
edgePoint,
bindableElement,
elementsMap,
zoom,
arrowElement,
)
: undefined;
const resolved = snapPoint || point;
const otherPoint = pointFrom<GlobalPoint>(
@@ -1599,121 +1614,6 @@ export const avoidRectangularCorner = (
return p;
};
export const snapToMid = (
bindTarget: ExcalidrawBindableElement,
elementsMap: ElementsMap,
p: GlobalPoint,
tolerance: number = 0.05,
arrowElement?: ExcalidrawArrowElement,
): GlobalPoint | undefined => {
const { x, y, width, height, angle } = bindTarget;
const center = elementCenterPoint(bindTarget, elementsMap, -0.1, -0.1);
const nonRotated = pointRotateRads(p, center, -angle as Radians);
const bindingGap = arrowElement ? getBindingGap(bindTarget, arrowElement) : 0;
// snap-to-center point is adaptive to element size, but we don't want to go
// above and below certain px distance
const verticalThreshold = clamp(tolerance * height, 5, 80);
const horizontalThreshold = clamp(tolerance * width, 5, 80);
// Too close to the center makes it hard to resolve direction precisely
if (pointDistance(center, nonRotated) < bindingGap) {
return undefined;
}
if (
nonRotated[0] <= x + width / 2 &&
nonRotated[1] > center[1] - verticalThreshold &&
nonRotated[1] < center[1] + verticalThreshold
) {
// LEFT
return pointRotateRads(
pointFrom<GlobalPoint>(x - bindingGap, center[1]),
center,
angle,
);
} else if (
nonRotated[1] <= y + height / 2 &&
nonRotated[0] > center[0] - horizontalThreshold &&
nonRotated[0] < center[0] + horizontalThreshold
) {
// TOP
return pointRotateRads(
pointFrom<GlobalPoint>(center[0], y - bindingGap),
center,
angle,
);
} else if (
nonRotated[0] >= x + width / 2 &&
nonRotated[1] > center[1] - verticalThreshold &&
nonRotated[1] < center[1] + verticalThreshold
) {
// RIGHT
return pointRotateRads(
pointFrom<GlobalPoint>(x + width + bindingGap, center[1]),
center,
angle,
);
} else if (
nonRotated[1] >= y + height / 2 &&
nonRotated[0] > center[0] - horizontalThreshold &&
nonRotated[0] < center[0] + horizontalThreshold
) {
// DOWN
return pointRotateRads(
pointFrom<GlobalPoint>(center[0], y + height + bindingGap),
center,
angle,
);
} else if (bindTarget.type === "diamond") {
const distance = bindingGap;
const topLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + height / 4 - distance,
);
const topRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + height / 4 - distance,
);
const bottomLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + (3 * height) / 4 + distance,
);
const bottomRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + (3 * height) / 4 + distance,
);
if (
pointDistance(topLeft, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(topLeft, center, angle);
}
if (
pointDistance(topRight, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(topRight, center, angle);
}
if (
pointDistance(bottomLeft, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(bottomLeft, center, angle);
}
if (
pointDistance(bottomRight, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(bottomRight, center, angle);
}
}
return undefined;
};
const extractBinding = (
arrow: ExcalidrawArrowElement,
startOrEnd: "startBinding" | "endBinding",
@@ -1913,6 +1813,7 @@ export const calculateFixedPointForElbowArrowBinding = (
hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end",
elementsMap: ElementsMap,
zoom: AppState["zoom"],
shouldSnapToOutline = true,
isMidpointSnappingEnabled = true,
): { fixedPoint: FixedPoint } => {
@@ -1928,6 +1829,7 @@ export const calculateFixedPointForElbowArrowBinding = (
hoveredElement,
startOrEnd,
elementsMap,
zoom,
undefined,
isMidpointSnappingEnabled,
)
+4
View File
@@ -1251,6 +1251,7 @@ const getElbowArrowData = (
"start",
arrow.startBinding?.fixedPoint,
origStartGlobalPoint,
options?.zoom || ({ value: 1 } as AppState["zoom"]),
hoveredStartElement,
elementsMap,
options?.isDragging,
@@ -1268,6 +1269,7 @@ const getElbowArrowData = (
"end",
arrow.endBinding?.fixedPoint,
origEndGlobalPoint,
options?.zoom || ({ value: 1 } as AppState["zoom"]),
hoveredEndElement,
elementsMap,
options?.isDragging,
@@ -2213,6 +2215,7 @@ const getGlobalPoint = (
startOrEnd: "start" | "end",
fixedPointRatio: [number, number] | undefined | null,
initialPoint: GlobalPoint,
zoom: AppState["zoom"],
element?: ExcalidrawBindableElement | null,
elementsMap?: ElementsMap,
isDragging?: boolean,
@@ -2226,6 +2229,7 @@ const getGlobalPoint = (
element,
startOrEnd,
elementsMap,
zoom,
undefined,
isMidpointSnappingEnabled,
);
+9 -1
View File
@@ -452,8 +452,16 @@ const createBindingArrow = (
"orbit",
"start",
scene,
appState.zoom,
);
bindBindingElement(
bindingArrow,
endBindingElement,
"orbit",
"end",
scene,
appState.zoom,
);
bindBindingElement(bindingArrow, endBindingElement, "orbit", "end", scene);
const changedElements = new Map<string, OrderedExcalidrawElement>();
changedElements.set(
+7 -4
View File
@@ -48,7 +48,6 @@ import {
calculateFixedPointForNonElbowArrowBinding,
getBindingStrategyForDraggingBindingElementEndpoints,
isBindingEnabled,
snapToMid,
updateBoundPoint,
} from "./binding";
import {
@@ -2200,13 +2199,15 @@ const pointDraggingUpdates = (
? {
element: suggestedBindingElement,
midPoint: app.state.isMidpointSnappingEnabled
? snapToMid(
suggestedBindingElement,
elementsMap,
? getSnapOutlineMidPoint(
pointFrom<GlobalPoint>(
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
),
suggestedBindingElement,
elementsMap,
app.state.zoom,
element,
)
: undefined,
}
@@ -2313,6 +2314,7 @@ const pointDraggingUpdates = (
start.element,
elementsMap,
app.state.zoom,
element,
),
}
: null;
@@ -2352,6 +2354,7 @@ const pointDraggingUpdates = (
end.element,
elementsMap,
app.state.zoom,
element,
),
}
: null;
+6
View File
@@ -17,6 +17,7 @@ import {
} from "@excalidraw/common";
import type { MarkOptional } from "@excalidraw/common/utility-types";
import type { Zoom } from "@excalidraw/excalidraw/types";
import { bindBindingElement } from "./binding";
import {
@@ -248,6 +249,7 @@ const bindLinearElementToElement = (
end: ValidLinearElement["end"],
elementStore: ElementStore,
scene: Scene,
zoom: Zoom,
): {
linearElement: ExcalidrawLinearElement;
startBoundElement?: ExcalidrawElement;
@@ -335,6 +337,7 @@ const bindLinearElementToElement = (
"orbit",
"start",
scene,
zoom,
);
}
}
@@ -411,6 +414,7 @@ const bindLinearElementToElement = (
"orbit",
"end",
scene,
zoom,
);
}
}
@@ -696,6 +700,7 @@ export const convertToExcalidrawElements = (
originalEnd,
elementStore,
scene,
{ value: 1 } as Zoom,
);
container = linearElement;
elementStore.add(linearElement);
@@ -721,6 +726,7 @@ export const convertToExcalidrawElements = (
end,
elementStore,
scene,
{ value: 1 } as Zoom,
);
elementStore.add(linearElement);
+146 -21
View File
@@ -8,6 +8,7 @@ import {
import {
bezierEquation,
clamp,
curve,
curveCatmullRomCubicApproxPoints,
curveOffsetPoints,
@@ -26,7 +27,7 @@ import {
type GlobalPoint,
} from "@excalidraw/math";
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
import type { Curve, LineSegment, LocalPoint, Radians } from "@excalidraw/math";
import type {
AppState,
@@ -41,7 +42,7 @@ import { generateLinearCollisionShape } from "./shape";
import { hitElementItself, isPointInElement } from "./collision";
import { LinearElementEditor } from "./linearElementEditor";
import { isRectangularElement } from "./typeChecks";
import { maxBindingDistance_simple } from "./binding";
import { getBindingGap, maxBindingDistance_simple } from "./binding";
import {
getGlobalFixedPointForBindableElement,
@@ -592,16 +593,135 @@ export const getSnapOutlineMidPoint = (
element: ExcalidrawBindableElement,
elementsMap: ElementsMap,
zoom: AppState["zoom"],
) => {
arrow: { elbowed: boolean },
): GlobalPoint | undefined => {
const center = elementCenterPoint(element, elementsMap);
const TOLERANCE = 0.05;
const maxDistance = maxBindingDistance_simple(zoom) + element.strokeWidth / 2;
const { x, y, width, height, angle } = element;
// snap-to-center point is adaptive to element size, but we don't want to go
// above and below certain px distance
const verticalThreshold = clamp(TOLERANCE * height, 5, maxDistance);
const horizontalThreshold = clamp(TOLERANCE * width, 5, maxDistance);
if (arrow.elbowed) {
const nonRotated = pointRotateRads(point, center, -angle as Radians);
const bindingGap = getBindingGap(element, arrow);
// Too close to the center makes it hard to resolve direction precisely
if (pointDistance(center, nonRotated) < bindingGap) {
return undefined;
}
if (
nonRotated[0] <= x + width / 2 &&
nonRotated[1] > center[1] - verticalThreshold &&
nonRotated[1] < center[1] + verticalThreshold
) {
// LEFT
return pointRotateRads(
pointFrom<GlobalPoint>(x - bindingGap, center[1]),
center,
angle,
);
} else if (
nonRotated[1] <= y + height / 2 &&
nonRotated[0] > center[0] - horizontalThreshold &&
nonRotated[0] < center[0] + horizontalThreshold
) {
// TOP
return pointRotateRads(
pointFrom<GlobalPoint>(center[0], y - bindingGap),
center,
angle,
);
} else if (
nonRotated[0] >= x + width / 2 &&
nonRotated[1] > center[1] - verticalThreshold &&
nonRotated[1] < center[1] + verticalThreshold
) {
// RIGHT
return pointRotateRads(
pointFrom<GlobalPoint>(x + width + bindingGap, center[1]),
center,
angle,
);
} else if (
nonRotated[1] >= y + height / 2 &&
nonRotated[0] > center[0] - horizontalThreshold &&
nonRotated[0] < center[0] + horizontalThreshold
) {
// DOWN
return pointRotateRads(
pointFrom<GlobalPoint>(center[0], y + height + bindingGap),
center,
angle,
);
} else if (element.type === "diamond") {
const distance = bindingGap;
const topLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + height / 4 - distance,
);
const topRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + height / 4 - distance,
);
const bottomLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + (3 * height) / 4 + distance,
);
const bottomRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + (3 * height) / 4 + distance,
);
if (
pointDistance(topLeft, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(topLeft, center, angle);
}
if (
pointDistance(topRight, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(topRight, center, angle);
}
if (
pointDistance(bottomLeft, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(bottomLeft, center, angle);
}
if (
pointDistance(bottomRight, nonRotated) <
Math.max(horizontalThreshold, verticalThreshold)
) {
return pointRotateRads(bottomRight, center, angle);
}
}
return undefined;
}
const sideMidpoints =
element.type === "diamond"
? getDiamondBaseCorners(element).map((curve) => {
const point = bezierEquation(curve, 0.5);
const rotatedPoint = pointRotateRads(point, center, element.angle);
? getDiamondBaseCorners(element)
.map((curve) => {
const point = bezierEquation(curve, 0.5);
const rotatedPoint = pointRotateRads(point, center, element.angle);
return pointFrom<GlobalPoint>(rotatedPoint[0], rotatedPoint[1]);
})
return pointFrom<GlobalPoint>(rotatedPoint[0], rotatedPoint[1]);
})
.map((midpoint) => {
return pointFrom<GlobalPoint>(
midpoint[0] + (midpoint[0] - center[0]) * 0.1,
midpoint[1] + (midpoint[1] - center[1]) * 0.1,
);
})
: [
// RIGHT midpoint
pointRotateRads(
@@ -634,20 +754,24 @@ export const getSnapOutlineMidPoint = (
element.angle,
),
];
const candidate = sideMidpoints.find(
(midpoint) =>
pointDistance(point, midpoint) <=
maxBindingDistance_simple(zoom) + element.strokeWidth / 2 &&
!hitElementItself({
point,
element,
threshold: 0,
elementsMap,
overrideShouldTestInside: true,
}),
);
return candidate;
return sideMidpoints
.map((midpoint, i) => {
const threshold = i % 2 === 0 ? horizontalThreshold : verticalThreshold;
return pointDistance(midpoint, point) <= threshold ? midpoint : undefined;
})
.find(
(midpoint) =>
midpoint &&
!hitElementItself({
point,
element,
threshold: 0,
elementsMap,
overrideShouldTestInside: true,
}),
);
};
export const projectFixedPointOntoDiagonal = (
@@ -670,6 +794,7 @@ export const projectFixedPointOntoDiagonal = (
element,
elementsMap,
zoom,
arrow,
);
if (sideMidPoint) {
return sideMidPoint;
+7 -2
View File
@@ -16,6 +16,7 @@ import "@excalidraw/utils/test-utils";
import { bindBindingElement } from "@excalidraw/element";
import type { LocalPoint } from "@excalidraw/math";
import type { Zoom } from "@excalidraw/excalidraw/types";
import { Scene } from "../src/Scene";
@@ -187,8 +188,12 @@ describe("elbow arrow routing", () => {
}) as ExcalidrawElbowArrowElement;
API.setElements([rectangle1, rectangle2, arrow]);
bindBindingElement(arrow, rectangle1, "orbit", "start", h.scene);
bindBindingElement(arrow, rectangle2, "orbit", "end", h.scene);
bindBindingElement(arrow, rectangle1, "orbit", "start", h.scene, {
value: 1,
} as Zoom);
bindBindingElement(arrow, rectangle2, "orbit", "end", h.scene, {
value: 1,
} as Zoom);
expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null);
@@ -1897,6 +1897,7 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
startElement,
"start",
elementsMap,
appState.zoom,
appState.isBindingEnabled,
),
}
@@ -1911,6 +1912,7 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
endElement,
"end",
elementsMap,
appState.zoom,
appState.isBindingEnabled,
),
}
@@ -1943,6 +1945,7 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
appState.bindMode === "inside" ? "inside" : "orbit",
"start",
app.scene,
appState.zoom,
);
}
}
@@ -1957,6 +1960,7 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
appState.bindMode === "inside" ? "inside" : "orbit",
"end",
app.scene,
appState.zoom,
);
}
}
+5
View File
@@ -7102,6 +7102,9 @@ class App extends React.Component<AppProps, AppState> {
hoveredElement,
elementsMap,
this.state.zoom,
{
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
},
),
},
});
@@ -7296,6 +7299,7 @@ class App extends React.Component<AppProps, AppState> {
hit,
elementsMap,
this.state.zoom,
{ elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow },
),
},
});
@@ -9490,6 +9494,7 @@ class App extends React.Component<AppProps, AppState> {
boundElement,
elementsMap,
this.state.zoom,
element,
),
}
: null,
@@ -17,7 +17,7 @@ import { EditorLocalStorage } from "../../data/EditorLocalStorage";
import type { MermaidToExcalidrawLibProps } from "./types";
import type { AppClassProperties, BinaryFiles } from "../../types";
import type { AppClassProperties, BinaryFiles, Zoom } from "../../types";
export const resetPreview = ({
canvasRef,
@@ -2374,7 +2374,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"endBinding": {
"elementId": "id1",
"fixedPoint": [
0,
"0.50010",
"0.50010",
],
"mode": "orbit",
@@ -2382,7 +2382,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "439.20000",
"height": "399.26547",
"id": "id4",
"index": "a2",
"isDeleted": false,
@@ -2396,8 +2396,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
488,
"-439.20000",
"488.00000",
"-399.26547",
],
],
"roughness": 1,
@@ -2419,9 +2419,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 11,
"width": 488,
"width": "488.00000",
"x": 6,
"y": "-5.39000",
"y": "-4.89900",
}
`;
@@ -2542,7 +2542,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"endBinding": {
"elementId": "id1",
"fixedPoint": [
0,
"0.50010",
"0.50010",
],
"mode": "orbit",
@@ -2550,7 +2550,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "439.20000",
"height": "399.26547",
"index": "a2",
"isDeleted": false,
"link": null,
@@ -2562,8 +2562,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
488,
"-439.20000",
"488.00000",
"-399.26547",
],
],
"roughness": 1,
@@ -2584,9 +2584,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"version": 11,
"width": 488,
"width": "488.00000",
"x": 6,
"y": "-5.39000",
"y": "-4.89900",
},
"inserted": {
"isDeleted": true,
@@ -16703,7 +16703,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"id": "id13",
"index": "a3",
"isDeleted": false,
@@ -16718,7 +16718,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -16729,8 +16729,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -16742,7 +16742,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 10,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
}
`;
@@ -16787,8 +16787,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -16807,8 +16807,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -17121,7 +17121,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"index": "a3",
"isDeleted": false,
"link": null,
@@ -17134,7 +17134,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -17145,8 +17145,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -17157,7 +17157,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 7,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
},
"inserted": {
"isDeleted": true,
@@ -17451,7 +17451,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"id": "id13",
"index": "a3",
"isDeleted": false,
@@ -17466,7 +17466,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -17477,8 +17477,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -17490,7 +17490,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 10,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
}
`;
@@ -17759,7 +17759,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"index": "a3",
"isDeleted": false,
"link": null,
@@ -17772,7 +17772,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -17783,8 +17783,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -17795,7 +17795,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 10,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
},
"inserted": {
"isDeleted": true,
@@ -18097,7 +18097,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"id": "id13",
"index": "a3",
"isDeleted": false,
@@ -18112,7 +18112,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -18123,8 +18123,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -18136,7 +18136,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 10,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
}
`;
@@ -18405,7 +18405,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"index": "a3",
"isDeleted": false,
"link": null,
@@ -18418,7 +18418,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -18429,8 +18429,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -18441,7 +18441,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 10,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
},
"inserted": {
"isDeleted": true,
@@ -18741,7 +18741,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"id": "id13",
"index": "a3",
"isDeleted": false,
@@ -18756,7 +18756,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -18767,8 +18767,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -18780,7 +18780,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 10,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
}
`;
@@ -18841,8 +18841,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -19135,7 +19135,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"index": "a3",
"isDeleted": false,
"link": null,
@@ -19148,7 +19148,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -19159,8 +19159,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -19171,7 +19171,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 7,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
},
"inserted": {
"isDeleted": true,
@@ -19493,7 +19493,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"id": "id13",
"index": "a3",
"isDeleted": false,
@@ -19508,7 +19508,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -19519,8 +19519,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -19532,7 +19532,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 11,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
}
`;
@@ -19604,8 +19604,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -19883,7 +19883,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"height": "0.00661",
"index": "a3",
"isDeleted": false,
"link": null,
@@ -19896,7 +19896,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
],
[
"88.00000",
0,
"-0.00661",
],
],
"roughness": 1,
@@ -19907,8 +19907,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.50021",
"0.50021",
],
"mode": "orbit",
},
@@ -19919,7 +19919,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 7,
"width": "88.00000",
"x": 6,
"y": "0.01000",
"y": "0.01706",
},
"inserted": {
"isDeleted": true,
@@ -182,14 +182,14 @@ exports[`move element > rectangles with binding arrow 7`] = `
"elementId": "id3",
"fixedPoint": [
"-0.02000",
"0.48010",
"0.47928",
],
"mode": "orbit",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "90.01760",
"height": "93.60377",
"id": "id6",
"index": "a2",
"isDeleted": false,
@@ -204,7 +204,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
],
[
89,
"90.01760",
"93.60377",
],
],
"roughness": 1,
@@ -217,7 +217,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
"elementId": "id0",
"fixedPoint": [
"1.06000",
"0.56011",
"0.52181",
],
"mode": "orbit",
},
@@ -230,6 +230,6 @@ exports[`move element > rectangles with binding arrow 7`] = `
"versionNonce": 271613161,
"width": 89,
"x": 106,
"y": "56.01120",
"y": "52.18052",
}
`;
+16 -6
View File
@@ -1590,7 +1590,9 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(5);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([1, 0.5001]),
fixedPoint: expect.arrayContaining([
0.5002127206977238, 0.5002127206977238,
]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1613,7 +1615,9 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(1);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([1, 0.5001]),
fixedPoint: expect.arrayContaining([
0.5002127206977238, 0.5002127206977238,
]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1636,7 +1640,9 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(0);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([1, 0.5001]),
fixedPoint: expect.arrayContaining([
0.5002127206977238, 0.5002127206977238,
]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1667,7 +1673,9 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(0);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([1, 0.5001]),
fixedPoint: expect.arrayContaining([
0.5002127206977238, 0.5002127206977238,
]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -1690,7 +1698,9 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(1);
expect(arrow.startBinding).toEqual({
elementId: rect1.id,
fixedPoint: expect.arrayContaining([1, 0.5001]),
fixedPoint: expect.arrayContaining([
0.5002127206977238, 0.5002127206977238,
]),
mode: "orbit",
});
expect(arrow.endBinding).toEqual({
@@ -5132,7 +5142,7 @@ describe("history", () => {
}),
endBinding: expect.objectContaining({
elementId: rect2.id,
fixedPoint: expect.arrayContaining([0, 0.5001]),
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
}),
isDeleted: true,
}),
+14 -4
View File
@@ -16,6 +16,8 @@ import * as StaticScene from "../renderer/staticScene";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { render, fireEvent, act, unmountComponent } from "./test-utils";
import type { Zoom } from "../types";
unmountComponent();
const renderInteractiveScene = vi.spyOn(
@@ -88,6 +90,7 @@ describe("move element", () => {
"orbit",
"start",
h.app.scene,
{ value: 1 } as Zoom,
);
bindBindingElement(
arrow.get() as NonDeleted<ExcalidrawArrowElement>,
@@ -95,6 +98,7 @@ describe("move element", () => {
"orbit",
"end",
h.app.scene,
{ value: 1 } as Zoom,
);
});
@@ -111,10 +115,13 @@ describe("move element", () => {
expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([200, 0]);
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints(
[[106.00000000000001, 55.6867741935484]],
[[106, 52.18052313249668]],
0,
);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints(
[[88, 91.60376557808824]],
0,
);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[88, 88]], 0);
renderInteractiveScene.mockClear();
renderStaticScene.mockClear();
@@ -133,10 +140,13 @@ describe("move element", () => {
expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([201, 2]);
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints(
[[106, 55.6867741935484]],
[[106, 52.18052313249668]],
0,
);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints(
[[89, 93.60376557808823]],
0,
);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[89, 90]], 0);
h.elements.forEach((element) => expect(element).toMatchSnapshot());
});