From 4a161c17640c0f705c48a4c8bbf50f3602508b7f Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Fri, 8 May 2026 13:58:30 +0000 Subject: [PATCH] fix: Centralized midpoint snap code Signed-off-by: Mark Tolmacs --- packages/element/src/arrows/focus.ts | 1 + packages/element/src/binding.ts | 134 ++------------ packages/element/src/elbowArrow.ts | 4 + packages/element/src/flowchart.ts | 10 +- packages/element/src/linearElementEditor.ts | 11 +- packages/element/src/transform.ts | 6 + packages/element/src/utils.ts | 167 +++++++++++++++--- packages/element/tests/elbowArrow.test.tsx | 9 +- .../excalidraw/actions/actionProperties.tsx | 4 + packages/excalidraw/components/App.tsx | 5 + .../excalidraw/components/TTDDialog/common.ts | 2 +- .../tests/__snapshots__/history.test.tsx.snap | 140 +++++++-------- .../tests/__snapshots__/move.test.tsx.snap | 10 +- packages/excalidraw/tests/history.test.tsx | 22 ++- packages/excalidraw/tests/move.test.tsx | 18 +- 15 files changed, 313 insertions(+), 230 deletions(-) diff --git a/packages/element/src/arrows/focus.ts b/packages/element/src/arrows/focus.ts index 8202777a26..f5d6e64530 100644 --- a/packages/element/src/arrows/focus.ts +++ b/packages/element/src/arrows/focus.ts @@ -269,6 +269,7 @@ export const handleFocusPointDrag = ( newMode || "orbit", linearElementEditor.draggedFocusPointBinding, scene, + appState.zoom, point, ); } diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index a1dd1bf8ec..07086b7514 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -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, 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( @@ -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(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(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(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(center[0], y + height + bindingGap), - center, - angle, - ); - } else if (bindTarget.type === "diamond") { - const distance = bindingGap; - const topLeft = pointFrom( - x + width / 4 - distance, - y + height / 4 - distance, - ); - const topRight = pointFrom( - x + (3 * width) / 4 + distance, - y + height / 4 - distance, - ); - const bottomLeft = pointFrom( - x + width / 4 - distance, - y + (3 * height) / 4 + distance, - ); - const bottomRight = pointFrom( - 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, ) diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index c0e53ee648..c7429b2d12 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -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, ); diff --git a/packages/element/src/flowchart.ts b/packages/element/src/flowchart.ts index daa98ed397..481adc1403 100644 --- a/packages/element/src/flowchart.ts +++ b/packages/element/src/flowchart.ts @@ -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(); changedElements.set( diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index d4b03a8840..e5bd98d672 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -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( 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; diff --git a/packages/element/src/transform.ts b/packages/element/src/transform.ts index 22828fe263..09483717fa 100644 --- a/packages/element/src/transform.ts +++ b/packages/element/src/transform.ts @@ -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); diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index 819cad562f..9354dcd1ea 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -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(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(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(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(center[0], y + height + bindingGap), + center, + angle, + ); + } else if (element.type === "diamond") { + const distance = bindingGap; + const topLeft = pointFrom( + x + width / 4 - distance, + y + height / 4 - distance, + ); + const topRight = pointFrom( + x + (3 * width) / 4 + distance, + y + height / 4 - distance, + ); + const bottomLeft = pointFrom( + x + width / 4 - distance, + y + (3 * height) / 4 + distance, + ); + const bottomRight = pointFrom( + 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(rotatedPoint[0], rotatedPoint[1]); - }) + return pointFrom(rotatedPoint[0], rotatedPoint[1]); + }) + .map((midpoint) => { + return pointFrom( + 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; diff --git a/packages/element/tests/elbowArrow.test.tsx b/packages/element/tests/elbowArrow.test.tsx index 2993e32158..3f03c6344a 100644 --- a/packages/element/tests/elbowArrow.test.tsx +++ b/packages/element/tests/elbowArrow.test.tsx @@ -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); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index a046db6ea8..e393d7de57 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1897,6 +1897,7 @@ export const actionChangeArrowType = register({ startElement, "start", elementsMap, + appState.zoom, appState.isBindingEnabled, ), } @@ -1911,6 +1912,7 @@ export const actionChangeArrowType = register({ endElement, "end", elementsMap, + appState.zoom, appState.isBindingEnabled, ), } @@ -1943,6 +1945,7 @@ export const actionChangeArrowType = register({ appState.bindMode === "inside" ? "inside" : "orbit", "start", app.scene, + appState.zoom, ); } } @@ -1957,6 +1960,7 @@ export const actionChangeArrowType = register({ appState.bindMode === "inside" ? "inside" : "orbit", "end", app.scene, + appState.zoom, ); } } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index e29dca5521..7db21377d2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -7102,6 +7102,9 @@ class App extends React.Component { hoveredElement, elementsMap, this.state.zoom, + { + elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, + }, ), }, }); @@ -7296,6 +7299,7 @@ class App extends React.Component { hit, elementsMap, this.state.zoom, + { elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow }, ), }, }); @@ -9490,6 +9494,7 @@ class App extends React.Component { boundElement, elementsMap, this.state.zoom, + element, ), } : null, diff --git a/packages/excalidraw/components/TTDDialog/common.ts b/packages/excalidraw/components/TTDDialog/common.ts index e02bafc1f6..5b7cd77f65 100644 --- a/packages/excalidraw/components/TTDDialog/common.ts +++ b/packages/excalidraw/components/TTDDialog/common.ts @@ -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, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 9320883697..37765a4bd0 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -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, diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index 5857132962..d9c7da5fdb 100644 --- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -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", } `; diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 78b431b773..8f8468289d 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -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, }), diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index f03e7744f1..cad39e198b 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -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, @@ -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()); });