From 4d1e2c2bbb92ba6aae70cb2fd2a3e22da2ef486a Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Mon, 24 Mar 2025 14:39:48 +0100 Subject: [PATCH] Revert to master --- excalidraw-app/components/DebugCanvas.tsx | 2 - packages/element/src/binding.ts | 133 +++++----- packages/element/src/collision.ts | 98 ++++---- packages/element/src/cropElement.ts | 2 +- packages/element/src/distance.ts | 16 +- packages/element/src/elbowArrow.ts | 3 - packages/element/src/linearElementEditor.ts | 91 ++----- packages/element/src/utils.ts | 18 +- packages/element/tests/binding.test.tsx | 13 +- packages/element/tests/elbowArrow.test.tsx | 37 +-- packages/element/tests/resize.test.tsx | 26 +- .../excalidraw/actions/actionFinalize.tsx | 22 +- .../excalidraw/actions/actionProperties.tsx | 2 - packages/excalidraw/components/App.tsx | 230 ++++++------------ .../data/__snapshots__/transform.test.ts.snap | 28 +-- packages/excalidraw/data/transform.test.ts | 6 +- .../tests/__snapshots__/history.test.tsx.snap | 196 +++++++-------- .../tests/__snapshots__/move.test.tsx.snap | 16 +- packages/excalidraw/tests/history.test.tsx | 4 +- .../tests/linearElementEditor.test.tsx | 2 +- packages/excalidraw/tests/move.test.tsx | 6 +- packages/excalidraw/tests/rotate.test.tsx | 10 +- packages/math/src/curve.ts | 21 +- packages/math/src/point.ts | 3 +- packages/math/src/utils.ts | 2 +- 25 files changed, 410 insertions(+), 577 deletions(-) diff --git a/excalidraw-app/components/DebugCanvas.tsx b/excalidraw-app/components/DebugCanvas.tsx index 383ef889b1..e83a62647d 100644 --- a/excalidraw-app/components/DebugCanvas.tsx +++ b/excalidraw-app/components/DebugCanvas.tsx @@ -13,8 +13,6 @@ import { useCallback, useImperativeHandle, useRef } from "react"; import { isLineSegment, - isCurve, - type Curve, type GlobalPoint, type LineSegment, } from "@excalidraw/math"; diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index da1c5f4dc7..7a67cf0a1f 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -463,10 +463,23 @@ export const maybeBindLinearElement = ( } }; -const normalizePointBinding = (binding: { focus: number; gap: number }) => { +const normalizePointBinding = ( + binding: { focus: number; gap: number }, + hoveredElement: ExcalidrawBindableElement, +) => { + let gap = binding.gap; + const maxGap = maxBindingGap( + hoveredElement, + hoveredElement.width, + hoveredElement.height, + ); + + if (gap > maxGap) { + gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET; + } return { ...binding, - gap: FIXED_BINDING_DISTANCE, + gap, }; }; @@ -716,7 +729,7 @@ const calculateFocusAndGap = ( return { focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), - gap: FIXED_BINDING_DISTANCE, + gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)), }; }; @@ -734,7 +747,7 @@ export const updateBoundElements = ( changedElements?: Map; }, ) => { - const { simultaneouslyUpdated } = options ?? {}; + const { newSize, simultaneouslyUpdated } = options ?? {}; const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( simultaneouslyUpdated, ); @@ -767,13 +780,23 @@ export const updateBoundElements = ( startBounds = getElementBounds(startBindingElement, elementsMap); endBounds = getElementBounds(endBindingElement, elementsMap); } + + const bindings = { + startBinding: maybeCalculateNewGapWhenScaling( + changedElement, + element.startBinding, + newSize, + ), + endBinding: maybeCalculateNewGapWhenScaling( + changedElement, + element.endBinding, + newSize, + ), + }; + // `linearElement` is being moved/scaled already, just update the binding if (simultaneouslyUpdatedElementIds.has(element.id)) { - mutateElement( - element, - { startBinding: element.startBinding, endBinding: element.endBinding }, - true, - ); + mutateElement(element, bindings, true); return; } @@ -795,9 +818,7 @@ export const updateBoundElements = ( const point = updateBoundPoint( element, bindingProp, - bindingProp === "startBinding" - ? element.startBinding - : element.endBinding, + bindings[bindingProp], bindableElement, elementsMap, ); @@ -917,7 +938,6 @@ export const bindPointToSnapToElementOutline = ( arrow: ExcalidrawElbowArrowElement, bindableElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", - elementsMap: ElementsMap, ): GlobalPoint => { if (isDevEnv() || isTestEnv()) { invariant(arrow.points.length > 1, "Arrow should have at least 2 points"); @@ -925,12 +945,10 @@ export const bindPointToSnapToElementOutline = ( const aabb = aabbForElement(bindableElement); const localP = - linearElement.points[ - startOrEnd === "start" ? 0 : linearElement.points.length - 1 - ]; + arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1]; const globalP = pointFrom( - linearElement.x + localP[0], - linearElement.y + localP[1], + arrow.x + localP[0], + arrow.y + localP[1], ); const edgePoint = isRectanguloidElement(bindableElement) ? avoidRectangularCorner(bindableElement, globalP) @@ -967,11 +985,7 @@ export const bindPointToSnapToElementOutline = ( ), otherPoint, ), - adjacentPoint, ), - ).sort( - (g, h) => - pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint), )[0]; } else { intersection = intersectElementWithLineSegment( @@ -1019,20 +1033,6 @@ export const bindPointToSnapToElementOutline = ( ); } - const currentDistance = pointDistance(edgePoint, center); - const fullDistance = Math.max( - pointDistance(intersection ?? edgePoint, center), - 1e-5, // Avoid division by zero - ); - - if (!isInside) { - return intersection; - } - - if (elbowed) { - return headingToMidBindPoint(edgePoint, bindableElement, aabb); - } - return edgePoint; }; @@ -1040,7 +1040,10 @@ export const avoidRectangularCorner = ( element: ExcalidrawBindableElement, p: GlobalPoint, ): GlobalPoint => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians); if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) { @@ -1310,24 +1313,6 @@ const updateBoundPoint = ( ), ]; - // debugClear(); - // intersections.forEach((intersection) => { - // debugDrawPoint(intersection, { permanent: true, color: "red" }); - // }); - // debugDrawLine( - // lineSegment( - // adjacentPoint, - // pointFromVector( - // vectorScale( - // vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)), - // interceptorLength, - // ), - // adjacentPoint, - // ), - // ), - // { permanent: true, color: "green" }, - // ); - if (intersections.length > 1) { // The adjacent point is outside the shape (+ gap) newEdgePoint = intersections[0]; @@ -1363,7 +1348,6 @@ export const calculateFixedPointForElbowArrowBinding = ( linearElement, hoveredElement, startOrEnd, - elementsMap, ); const globalMidPoint = pointFrom( bounds[0] + (bounds[2] - bounds[0]) / 2, @@ -1385,6 +1369,28 @@ export const calculateFixedPointForElbowArrowBinding = ( }; }; +const maybeCalculateNewGapWhenScaling = ( + changedElement: ExcalidrawBindableElement, + currentBinding: PointBinding | null | undefined, + newSize: { width: number; height: number } | undefined, +): PointBinding | null | undefined => { + if (currentBinding == null || newSize == null) { + return currentBinding; + } + const { width: newWidth, height: newHeight } = newSize; + const { width, height } = changedElement; + const newGap = Math.max( + 1, + Math.min( + maxBindingGap(changedElement, newWidth, newHeight), + currentBinding.gap * + (newWidth < newHeight ? newWidth / width : newHeight / height), + ), + ); + + return { ...currentBinding, gap: newGap }; +}; + const getElligibleElementForBindingElement = ( linearElement: NonDeleted, startOrEnd: "start" | "end", @@ -1765,7 +1771,10 @@ const determineFocusDistance = ( // Another point on the line, in absolute coordinates (closer to element) b: GlobalPoint, ): number => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); if (pointsEqual(a, b)) { return 0; @@ -1895,7 +1904,10 @@ const determineFocusPoint = ( focus: number, adjacentPoint: GlobalPoint, ): GlobalPoint => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); if (focus === 0) { return center; @@ -2326,7 +2338,10 @@ export const getGlobalFixedPointForBindableElement = ( element.x + element.width * fixedX, element.y + element.height * fixedY, ), - elementCenterPoint(element), + pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ), element.angle, ); }; diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 14c9145a16..1c830b6f3c 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -41,7 +41,6 @@ import { import { deconstructDiamondElement, deconstructRectanguloidElement, - elementCenterPoint, } from "./utils"; import type { @@ -192,7 +191,10 @@ const intersectRectanguloidWithLineSegment = ( l: LineSegment, offset: number = 0, ): GlobalPoint[] => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); // To emulate a rotated rectangle we rotate the point in the inverse angle // instead. It's all the same distance-wise. const rotatedA = pointRotateRads( @@ -210,32 +212,31 @@ const intersectRectanguloidWithLineSegment = ( const [sides, corners] = deconstructRectanguloidElement(element, offset); return ( - // Test intersection against the sides, keep only the valid - // intersection points and rotate them back to scene space - sides - .map((s) => - lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ), - ) - .filter((x) => x != null) - .map((j) => pointRotateRads(j!, center, element.angle)) + [ + // Test intersection against the sides, keep only the valid + // intersection points and rotate them back to scene space + ...sides + .map((s) => + lineSegmentIntersectionPoints( + lineSegment(rotatedA, rotatedB), + s, + ), + ) + .filter((x) => x != null) + .map((j) => pointRotateRads(j!, center, element.angle)), // Test intersection against the corners which are cubic bezier curves, // keep only the valid intersection points and rotate them back to scene // space - .concat( - corners - .flatMap((t) => - curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)), - ) - .filter((i) => i != null) - .map((j) => pointRotateRads(j, center, element.angle)), - ) + ...corners + .flatMap((t) => + curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)), + ) + .filter((i) => i != null) + .map((j) => pointRotateRads(j, center, element.angle)), + ] // Remove duplicates .filter( - (p, idx, points) => - points.findIndex((d) => pointsEqual(p, d, 1e-3)) === idx, + (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx, ) ); }; @@ -252,7 +253,10 @@ const intersectDiamondWithLineSegment = ( l: LineSegment, offset: number = 0, ): GlobalPoint[] => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); // Rotate the point to the inverse direction to simulate the rotated diamond // points. It's all the same distance-wise. @@ -262,29 +266,28 @@ const intersectDiamondWithLineSegment = ( const [sides, curves] = deconstructDiamondElement(element, offset); return ( - sides - .map((s) => - lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ), - ) - .filter((p): p is GlobalPoint => p != null) - // Rotate back intersection points - .map((p) => pointRotateRads(p!, center, element.angle)) - .concat( - curves - .flatMap((p) => - curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)), - ) - .filter((p) => p != null) - // Rotate back intersection points - .map((p) => pointRotateRads(p, center, element.angle)), - ) + [ + ...sides + .map((s) => + lineSegmentIntersectionPoints( + lineSegment(rotatedA, rotatedB), + s, + ), + ) + .filter((p): p is GlobalPoint => p != null) + // Rotate back intersection points + .map((p) => pointRotateRads(p!, center, element.angle)), + ...curves + .flatMap((p) => + curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)), + ) + .filter((p) => p != null) + // Rotate back intersection points + .map((p) => pointRotateRads(p, center, element.angle)), + ] // Remove duplicates .filter( - (p, idx, points) => - points.findIndex((d) => pointsEqual(p, d, 1e-3)) === idx, + (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx, ) ); }; @@ -301,7 +304,10 @@ const intersectEllipseWithLineSegment = ( l: LineSegment, offset: number = 0, ): GlobalPoint[] => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); diff --git a/packages/element/src/cropElement.ts b/packages/element/src/cropElement.ts index 6befc67d1f..dd75f9360f 100644 --- a/packages/element/src/cropElement.ts +++ b/packages/element/src/cropElement.ts @@ -61,7 +61,7 @@ export const cropElement = ( const rotatedPointer = pointRotateRads( pointFrom(pointerX, pointerY), - elementCenterPoint(element), + pointFrom(element.x + element.width / 2, element.y + element.height / 2), -element.angle as Radians, ); diff --git a/packages/element/src/distance.ts b/packages/element/src/distance.ts index 871ceb085f..d9db939e4d 100644 --- a/packages/element/src/distance.ts +++ b/packages/element/src/distance.ts @@ -1,6 +1,7 @@ import { curvePointDistance, distanceToLineSegment, + pointFrom, pointRotateRads, } from "@excalidraw/math"; @@ -52,7 +53,10 @@ const distanceToRectanguloidElement = ( element: ExcalidrawRectanguloidElement, p: GlobalPoint, ) => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); // To emulate a rotated rectangle we rotate the point in the inverse angle // instead. It's all the same distance-wise. const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians); @@ -80,7 +84,10 @@ const distanceToDiamondElement = ( element: ExcalidrawDiamondElement, p: GlobalPoint, ): number => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); // Rotate the point to the inverse direction to simulate the rotated diamond // points. It's all the same distance-wise. @@ -108,7 +115,10 @@ const distanceToEllipseElement = ( element: ExcalidrawEllipseElement, p: GlobalPoint, ): number => { - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); return ellipseDistanceFromPoint( // Instead of rotating the ellipse, rotate the point to the inverse angle pointRotateRads(p, center, -element.angle as Radians), diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index db746fbc31..a70e265bc6 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -1249,7 +1249,6 @@ const getElbowArrowData = ( ...arrow, type: "arrow", elbowed: true, - type: "arrow", points: nextPoints, } as ExcalidrawElbowArrowElement, "start", @@ -1263,7 +1262,6 @@ const getElbowArrowData = ( ...arrow, type: "arrow", elbowed: true, - type: "arrow", points: nextPoints, } as ExcalidrawElbowArrowElement, "end", @@ -2223,7 +2221,6 @@ const getGlobalPoint = ( arrow, element, startOrEnd, - elementsMap, ); return snapToMid(element, snapPoint); diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index e8a671fa52..8a9117bf88 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -239,43 +239,6 @@ export class LinearElementEditor { }); } - static getOutlineAvoidingPoint( - element: NonDeleted, - coords: GlobalPoint, - pointIndex: number, - app: AppClassProperties, - fallback?: GlobalPoint, - ): GlobalPoint { - const hoveredElement = getHoveredElementForBinding( - { x: coords[0], y: coords[1] }, - app.scene.getNonDeletedElements(), - app.scene.getNonDeletedElementsMap(), - app.state.zoom, - true, - isElbowArrow(element), - ); - - if (hoveredElement) { - const newPoints = Array.from(element.points); - newPoints[pointIndex] = pointFrom( - coords[0] - element.x, - coords[1] - element.y, - ); - - return bindPointToSnapToElementOutline( - { - ...element, - points: newPoints, - }, - hoveredElement, - pointIndex === 0 ? "start" : "end", - app.scene.getNonDeletedElementsMap(), - ); - } - - return fallback ?? coords; - } - /** * @returns whether point was dragged */ @@ -295,16 +258,14 @@ export class LinearElementEditor { return null; } const { elementId } = linearElementEditor; - const elementsMap = app.scene.getNonDeletedElementsMap(); + const elementsMap = scene.getNonDeletedElementsMap(); const element = LinearElementEditor.getElement(elementId, elementsMap); if (!element) { return null; } - const elbowed = isElbowArrow(element); - if ( - elbowed && + isElbowArrow(element) && !linearElementEditor.pointerDownState.lastClickedIsEndPoint && linearElementEditor.pointerDownState.lastClickedPoint !== 0 ) { @@ -321,7 +282,7 @@ export class LinearElementEditor { : undefined, ].filter((idx): idx is number => idx !== undefined) : linearElementEditor.selectedPointsIndices; - const lastClickedPoint = elbowed + const lastClickedPoint = isElbowArrow(element) ? linearElementEditor.pointerDownState.lastClickedPoint > 0 ? element.points.length - 1 : 0 @@ -373,43 +334,19 @@ export class LinearElementEditor { LinearElementEditor.movePoints( element, selectedPointsIndices.map((pointIndex) => { - let newPointPosition = pointFrom( - element.points[pointIndex][0] + deltaX, - element.points[pointIndex][1] + deltaY, - ); - - // Check if point dragging is happening - if (pointIndex === lastClickedPoint) { - let globalNewPointPosition = pointFrom( - scenePointerX - linearElementEditor.pointerOffset.x, - scenePointerY - linearElementEditor.pointerOffset.y, - ); - - if ( - pointIndex === 0 || - pointIndex === element.points.length - 1 - ) { - globalNewPointPosition = - LinearElementEditor.getOutlineAvoidingPoint( + const newPointPosition: LocalPoint = + pointIndex === lastClickedPoint + ? LinearElementEditor.createPointAt( element, - pointFrom( - element.x + element.points[pointIndex][0] + deltaX, - element.y + element.points[pointIndex][1] + deltaY, - ), - pointIndex, - app, + elementsMap, + scenePointerX - linearElementEditor.pointerOffset.x, + scenePointerY - linearElementEditor.pointerOffset.y, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), + ) + : pointFrom( + element.points[pointIndex][0] + deltaX, + element.points[pointIndex][1] + deltaY, ); - } - - newPointPosition = LinearElementEditor.createPointAt( - element, - elementsMap, - globalNewPointPosition[0], - globalNewPointPosition[1], - event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), - ); - } - return { index: pointIndex, point: newPointPosition, diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index 05228e2aa1..7042b5d8f1 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -18,7 +18,6 @@ import { getDiamondPoints } from "./bounds"; import type { ExcalidrawDiamondElement, - ExcalidrawElement, ExcalidrawRectanguloidElement, } from "./types"; @@ -69,7 +68,10 @@ export function deconstructRectanguloidElement( return [sides, []]; } - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); const r = rectangle( pointFrom(element.x, element.y), @@ -252,7 +254,10 @@ export function deconstructDiamondElement( return [[topRight, bottomRight, bottomLeft, topLeft], []]; } - const center = elementCenterPoint(element); + const center = pointFrom( + element.x + element.width / 2, + element.y + element.height / 2, + ); const [top, right, bottom, left]: GlobalPoint[] = [ pointFrom(element.x + topX, element.y + topY), @@ -352,10 +357,3 @@ export function deconstructDiamondElement( return [sides, corners]; } - -export function elementCenterPoint(element: ExcalidrawElement) { - return pointFrom( - element.x + element.width / 2, - element.y + element.height / 2, - ); -} diff --git a/packages/element/tests/binding.test.tsx b/packages/element/tests/binding.test.tsx index ff3c8d10ad..f57d7793ae 100644 --- a/packages/element/tests/binding.test.tsx +++ b/packages/element/tests/binding.test.tsx @@ -190,18 +190,7 @@ describe("element binding", () => { // Sever connection expect(API.getSelectedElement().type).toBe("arrow"); - Keyboard.withModifierKeys({ shift: true }, () => { - // We have to move a significant distance to get out of the binding zone - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - Keyboard.keyPress(KEYS.ARROW_LEFT); - }); + Keyboard.keyPress(KEYS.ARROW_LEFT); expect(arrow.endBinding).toBe(null); Keyboard.keyPress(KEYS.ARROW_RIGHT); expect(arrow.endBinding).toBe(null); diff --git a/packages/element/tests/elbowArrow.test.tsx b/packages/element/tests/elbowArrow.test.tsx index a4756c26fb..b8b5a8b85d 100644 --- a/packages/element/tests/elbowArrow.test.tsx +++ b/packages/element/tests/elbowArrow.test.tsx @@ -77,9 +77,9 @@ describe("elbow arrow segment move", () => { expect(arrow.points).toCloselyEqualPoints([ [0, 0], - [107.93, 0], - [107.93, 185.86], - [185.86, 185.86], + [110, 0], + [110, 200], + [190, 200], ]); mouse.reset(); @@ -88,9 +88,9 @@ describe("elbow arrow segment move", () => { expect(arrow.points).toCloselyEqualPoints([ [0, 0], - [107.93, 0], - [107.93, 185.86], - [185.86, 185.86], + [110, 0], + [110, 200], + [190, 200], ]); }); @@ -198,11 +198,11 @@ describe("elbow arrow routing", () => { points: [pointFrom(0, 0), pointFrom(90, 200)], }); - expect(arrow.points).toCloselyEqualPoints([ + expect(arrow.points).toEqual([ [0, 0], - [42.93, 0], - [42.93, 195.7], - [85.86, 195.7], + [45, 0], + [45, 200], + [90, 200], ]); }); }); @@ -241,9 +241,9 @@ describe("elbow arrow ui", () => { expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow); mouse.reset(); - mouse.moveTo(-50, -100); + mouse.moveTo(-43, -99); mouse.click(); - mouse.moveTo(50, 100); + mouse.moveTo(43, 99); mouse.click(); const arrow = h.scene.getSelectedElements( @@ -252,11 +252,11 @@ describe("elbow arrow ui", () => { expect(arrow.type).toBe("arrow"); expect(arrow.elbowed).toBe(true); - expect(arrow.points).toCloselyEqualPoints([ + expect(arrow.points).toEqual([ [0, 0], - [42.93, 0], - [42.93, 153.48], - [85.86, 153.48], + [45, 0], + [45, 200], + [90, 200], ]); }); @@ -296,8 +296,9 @@ describe("elbow arrow ui", () => { expect(arrow.points.map((point) => point.map(Math.round))).toEqual([ [0, 0], - [129, 0], - [129, 131], + [35, 0], + [35, 165], + [103, 165], ]); }); diff --git a/packages/element/tests/resize.test.tsx b/packages/element/tests/resize.test.tsx index 1a325b589a..f3804e2a22 100644 --- a/packages/element/tests/resize.test.tsx +++ b/packages/element/tests/resize.test.tsx @@ -195,7 +195,7 @@ describe("generic element", () => { UI.resize(rectangle, "w", [50, 0]); expect(arrow.endBinding?.elementId).toEqual(rectangle.id); - expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80.62, 0); + expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0); }); it("resizes with a label", async () => { @@ -510,13 +510,13 @@ describe("arrow element", () => { h.state, )[0] as ExcalidrawElbowArrowElement; - expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07); - expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); UI.resize(rectangle, "se", [-200, -150]); - expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07); - expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); }); it("flips the fixed point binding on negative resize for group selection", () => { @@ -538,8 +538,8 @@ describe("arrow element", () => { h.state, )[0] as ExcalidrawElbowArrowElement; - expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07); - expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); UI.resize([rectangle, arrow], "nw", [300, 350]); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0); @@ -809,7 +809,7 @@ describe("image element", () => { }); API.setElements([image]); const arrow = UI.createElement("arrow", { - x: -29, + x: -30, y: 50, width: 28, height: 5, @@ -819,14 +819,14 @@ describe("image element", () => { UI.resize(image, "ne", [40, 0]); - expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0); + expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0); const imageWidth = image.width; const scale = 20 / image.height; UI.resize(image, "nw", [50, 20]); expect(arrow.endBinding?.elementId).toEqual(image.id); - expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo( + expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo( 30 + imageWidth * scale, 0, ); @@ -1033,11 +1033,11 @@ describe("multiple selection", () => { expect(leftBoundArrow.x).toBeCloseTo(-110); expect(leftBoundArrow.y).toBeCloseTo(50); - expect(leftBoundArrow.width).toBeCloseTo(146.46, 0); + expect(leftBoundArrow.width).toBeCloseTo(143, 0); expect(leftBoundArrow.height).toBeCloseTo(7, 0); expect(leftBoundArrow.angle).toEqual(0); expect(leftBoundArrow.startBinding).toBeNull(); - expect(leftBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE); + expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10); expect(leftBoundArrow.endBinding?.elementId).toBe( leftArrowBinding.elementId, ); @@ -1051,7 +1051,7 @@ describe("multiple selection", () => { expect(rightBoundArrow.height).toBeCloseTo(0); expect(rightBoundArrow.angle).toEqual(0); expect(rightBoundArrow.startBinding).toBeNull(); - expect(rightBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE); + expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952); expect(rightBoundArrow.endBinding?.elementId).toBe( rightArrowBinding.elementId, ); diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index 0da63a29e6..9849616562 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -91,26 +91,10 @@ export const actionFinalize = register({ multiPointElement.type !== "freedraw" && appState.lastPointerDownWith !== "touch" ) { - const { x: rx, y: ry, points, lastCommittedPoint } = multiPointElement; - const lastGlobalPoint = pointFrom( - rx + points[points.length - 1][0], - ry + points[points.length - 1][1], - ); - const hoveredElementForBinding = getHoveredElementForBinding( - { - x: lastGlobalPoint[0], - y: lastGlobalPoint[1], - }, - elements, - elementsMap, - app.state.zoom, - true, - isElbowArrow(multiPointElement), - ); + const { points, lastCommittedPoint } = multiPointElement; if ( - !hoveredElementForBinding && - (!lastCommittedPoint || - points[points.length - 1] !== lastCommittedPoint) + !lastCommittedPoint || + points[points.length - 1] !== lastCommittedPoint ) { mutateElement(multiPointElement, { points: multiPointElement.points.slice(0, -1), diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index d3a01acf3c..5a309b6775 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1655,7 +1655,6 @@ export const actionChangeArrowType = register({ newElement, startHoveredElement, "start", - elementsMap, ) : startGlobalPoint; const finalEndPoint = endHoveredElement @@ -1663,7 +1662,6 @@ export const actionChangeArrowType = register({ newElement, endHoveredElement, "end", - elementsMap, ) : endGlobalPoint; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 600ddadbca..276cde0274 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -5991,25 +5991,15 @@ class App extends React.Component { if (isPathALoop(points, this.state.zoom.value)) { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } - // update last uncommitted point mutateElement( multiElement, { points: [ ...points.slice(0, -1), - pointTranslate( - LinearElementEditor.getOutlineAvoidingPoint( - multiElement, - pointFrom(scenePointerX, scenePointerY), - multiElement.points.length - 1, - this, - pointFrom( - multiElement.x + lastCommittedX + dxFromLastCommitted, - multiElement.y + lastCommittedY + dyFromLastCommitted, - ), - ), - vector(-multiElement.x, -multiElement.y), + pointFrom( + lastCommittedX + dxFromLastCommitted, + lastCommittedY + dyFromLastCommitted, ), ], }, @@ -7761,34 +7751,18 @@ class App extends React.Component { } const { x: rx, y: ry, lastCommittedPoint } = multiElement; - const lastGlobalPoint = pointFrom( - rx + multiElement.points[multiElement.points.length - 1][0], - ry + multiElement.points[multiElement.points.length - 1][1], - ); - const hoveredElementForBinding = getHoveredElementForBinding( - { - x: lastGlobalPoint[0], - y: lastGlobalPoint[1], - }, - this.scene.getNonDeletedElements(), - this.scene.getNonDeletedElementsMap(), - this.state.zoom, - true, - isElbowArrow(multiElement), - ); // clicking inside commit zone → finalize arrow if ( - !!hoveredElementForBinding || - (multiElement.points.length > 1 && - lastCommittedPoint && - pointDistance( - pointFrom( - pointerDownState.origin.x - rx, - pointerDownState.origin.y - ry, - ), - lastCommittedPoint, - ) < LINE_CONFIRM_THRESHOLD) + multiElement.points.length > 1 && + lastCommittedPoint && + pointDistance( + pointFrom( + pointerDownState.origin.x - rx, + pointerDownState.origin.y - ry, + ), + lastCommittedPoint, + ) < LINE_CONFIRM_THRESHOLD ) { this.actionManager.executeAction(actionFinalize); return; @@ -7832,93 +7806,53 @@ class App extends React.Component { ? [currentItemStartArrowhead, currentItemEndArrowhead] : [null, null]; - let element: NonDeleted; - if (elementType === "arrow") { - const arrow: Mutable> = - newArrowElement({ - type: "arrow", - x: gridX, - y: gridY, - strokeColor: this.state.currentItemStrokeColor, - backgroundColor: this.state.currentItemBackgroundColor, - fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, - strokeStyle: this.state.currentItemStrokeStyle, - roughness: this.state.currentItemRoughness, - opacity: this.state.currentItemOpacity, - roundness: - this.state.currentItemArrowType === ARROW_TYPE.round - ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } - : // note, roundness doesn't have any effect for elbow arrows, - // but it's best to set it to null as well - null, - startArrowhead, - endArrowhead, - locked: false, - frameId: topLayerFrame ? topLayerFrame.id : null, - elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, - fixedSegments: - this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null, - }); - - const hoveredElement = getHoveredElementForBinding( - { x: gridX, y: gridY }, - this.scene.getNonDeletedElements(), - this.scene.getNonDeletedElementsMap(), - this.state.zoom, - true, - this.state.currentItemArrowType === ARROW_TYPE.elbow, - ); - - if (hoveredElement) { - [arrow.x, arrow.y] = - intersectElementWithLineSegment( - hoveredElement, - lineSegment( - pointFrom(gridX, gridY), - pointFrom( - gridX, - hoveredElement.y + hoveredElement.height / 2, - ), - ), - 2 * FIXED_BINDING_DISTANCE, - )[0] ?? - intersectElementWithLineSegment( - hoveredElement, - lineSegment( - pointFrom(gridX, gridY), - pointFrom( - hoveredElement.x + hoveredElement.width / 2, - gridY, - ), - ), - 2 * FIXED_BINDING_DISTANCE, - )[0] ?? - pointFrom(gridX, gridY); - } - - element = arrow; - } else { - element = newLinearElement({ - type: elementType, - x: gridX, - y: gridY, - strokeColor: this.state.currentItemStrokeColor, - backgroundColor: this.state.currentItemBackgroundColor, - fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, - strokeStyle: this.state.currentItemStrokeStyle, - roughness: this.state.currentItemRoughness, - opacity: this.state.currentItemOpacity, - roundness: - this.state.currentItemRoundness === "round" - ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } - : null, - locked: false, - frameId: topLayerFrame ? topLayerFrame.id : null, - }); - } - + const element = + elementType === "arrow" + ? newArrowElement({ + type: elementType, + x: gridX, + y: gridY, + strokeColor: this.state.currentItemStrokeColor, + backgroundColor: this.state.currentItemBackgroundColor, + fillStyle: this.state.currentItemFillStyle, + strokeWidth: this.state.currentItemStrokeWidth, + strokeStyle: this.state.currentItemStrokeStyle, + roughness: this.state.currentItemRoughness, + opacity: this.state.currentItemOpacity, + roundness: + this.state.currentItemArrowType === ARROW_TYPE.round + ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } + : // note, roundness doesn't have any effect for elbow arrows, + // but it's best to set it to null as well + null, + startArrowhead, + endArrowhead, + locked: false, + frameId: topLayerFrame ? topLayerFrame.id : null, + elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, + fixedSegments: + this.state.currentItemArrowType === ARROW_TYPE.elbow + ? [] + : null, + }) + : newLinearElement({ + type: elementType, + x: gridX, + y: gridY, + strokeColor: this.state.currentItemStrokeColor, + backgroundColor: this.state.currentItemBackgroundColor, + fillStyle: this.state.currentItemFillStyle, + strokeWidth: this.state.currentItemStrokeWidth, + strokeStyle: this.state.currentItemStrokeStyle, + roughness: this.state.currentItemRoughness, + opacity: this.state.currentItemOpacity, + roundness: + this.state.currentItemRoundness === "round" + ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } + : null, + locked: false, + frameId: topLayerFrame ? topLayerFrame.id : null, + }); this.setState((prevState) => { const nextSelectedElementIds = { ...prevState.selectedElementIds, @@ -8233,6 +8167,12 @@ class App extends React.Component { this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y); } + const [gridX, gridY] = getGridPoint( + pointerCoords.x, + pointerCoords.y, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), + ); + // for arrows/lines, don't start dragging until a given threshold // to ensure we don't create a 2-point arrow by mistake when // user clicks mouse in a way that it moves a tiny bit (thus @@ -8333,6 +8273,7 @@ class App extends React.Component { ); }, linearElementEditor, + this.scene, ); if (newLinearElementEditor) { pointerDownState.lastCoords.x = pointerCoords.x; @@ -8719,11 +8660,6 @@ class App extends React.Component { } else if (isLinearElement(newElement)) { pointerDownState.drag.hasOccurred = true; const points = newElement.points; - const [gridX, gridY] = getGridPoint( - pointerCoords.x, - pointerCoords.y, - event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), - ); let dx = gridX - newElement.x; let dy = gridY - newElement.y; @@ -8740,22 +8676,7 @@ class App extends React.Component { mutateElement( newElement, { - points: [ - ...points, - pointTranslate( - LinearElementEditor.getOutlineAvoidingPoint( - newElement, - pointFrom(pointerCoords.x, pointerCoords.y), - newElement.points.length - 1, - this, - pointFrom( - newElement.x + dx, - newElement.y + dy, - ), - ), - vector(-newElement.x, -newElement.y), - ), - ], + points: [...points, pointFrom(dx, dy)], }, false, ); @@ -8766,22 +8687,7 @@ class App extends React.Component { mutateElement( newElement, { - points: [ - ...points.slice(0, -1), - pointTranslate( - LinearElementEditor.getOutlineAvoidingPoint( - newElement, - pointFrom(pointerCoords.x, pointerCoords.y), - newElement.points.length - 1, - this, - pointFrom( - newElement.x + dx, - newElement.y + dy, - ), - ), - vector(-newElement.x, -newElement.y), - ), - ], + points: [...points.slice(0, -1), pointFrom(dx, dy)], }, false, { isDragging: true }, diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap index 00331bd11f..70f8daa313 100644 --- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -89,7 +89,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "endBinding": { "elementId": "ellipse-1", "focus": -0.007519379844961235, - "gap": 5, + "gap": 11.562288374879595, }, "fillStyle": "solid", "frameId": null, @@ -119,7 +119,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "startBinding": { "elementId": "id49", "focus": -0.0813953488372095, - "gap": 5, + "gap": 1, }, "strokeColor": "#1864ab", "strokeStyle": "solid", @@ -145,7 +145,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "endBinding": { "elementId": "ellipse-1", "focus": 0.10666666666666667, - "gap": 5, + "gap": 3.8343264684446097, }, "fillStyle": "solid", "frameId": null, @@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "startBinding": { "elementId": "diamond-1", "focus": 0, - "gap": 5, + "gap": 4.545343408287929, }, "strokeColor": "#e67700", "strokeStyle": "solid", @@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "endBinding": { "elementId": "text-2", "focus": 0, - "gap": 5, + "gap": 14, }, "fillStyle": "solid", "frameId": null, @@ -365,7 +365,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "startBinding": { "elementId": "text-1", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -437,7 +437,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "endBinding": { "elementId": "id42", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -467,7 +467,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "startBinding": { "elementId": "id41", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -613,7 +613,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "endBinding": { "elementId": "id46", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -643,7 +643,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "startBinding": { "elementId": "id45", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "endBinding": { "elementId": "Alice", "focus": -0, - "gap": 5, + "gap": 5.299874999999986, }, "fillStyle": "solid", "frameId": null, @@ -1507,7 +1507,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "startBinding": { "elementId": "Bob", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1538,7 +1538,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "endBinding": { "elementId": "B", "focus": 0, - "gap": 5, + "gap": 14, }, "fillStyle": "solid", "frameId": null, @@ -1566,7 +1566,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "startBinding": { "elementId": "Bob", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts index 75c8b40643..0b0718e8e3 100644 --- a/packages/excalidraw/data/transform.test.ts +++ b/packages/excalidraw/data/transform.test.ts @@ -433,7 +433,7 @@ describe("Test Transform", () => { startBinding: { elementId: rectangle.id, focus: 0, - gap: FIXED_BINDING_DISTANCE, + gap: 1, }, endBinding: { elementId: ellipse.id, @@ -518,7 +518,7 @@ describe("Test Transform", () => { startBinding: { elementId: text2.id, focus: 0, - gap: FIXED_BINDING_DISTANCE, + gap: 1, }, endBinding: { elementId: text3.id, @@ -781,7 +781,7 @@ describe("Test Transform", () => { expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({ elementId: "rect-1", focus: -0, - gap: FIXED_BINDING_DISTANCE, + gap: 14, }); expect(rect.boundElements).toStrictEqual([ { diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 8f4fc42e94..9ffb97128a 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "99.58947", + "height": "102.35417", "id": "id172", "index": "a2", "isDeleted": false, @@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "99.58947", - "99.58947", + "101.77517", + "102.35417", ], ], "roughness": 1, @@ -228,8 +228,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 40, - "width": "99.58947", - "x": 0, + "width": "101.77517", + "x": "0.70711", "y": 0, } `; @@ -296,49 +296,47 @@ History { "endBinding": { "elementId": "id171", "focus": "0.00990", - "gap": 5, + "gap": 1, }, - "height": "0.92929", + "height": "0.98586", "points": [ [ 0, 0, ], [ - "92.92893", - "-0.92929", + "98.58579", + "-0.98586", ], ], "startBinding": { "elementId": "id170", "focus": "0.02970", - "gap": 5, + "gap": 1, }, - "width": "92.92893", }, "inserted": { "endBinding": { "elementId": "id171", - "focus": "-0.02075", - "gap": 5, + "focus": "-0.02000", + "gap": 1, }, - "height": "0.07074", + "height": "0.00000", "points": [ [ 0, 0, ], [ - "92.92893", - "0.07074", + "98.58579", + "0.00000", ], ], "startBinding": { "elementId": "id170", - "focus": "0.01770", - "gap": 5, + "focus": "0.02000", + "gap": 1, }, - "width": "92.92893", }, }, }, @@ -392,47 +390,43 @@ History { "focus": 0, "gap": 1, }, - "height": "99.58947", + "height": "102.35417", "points": [ [ 0, 0, ], [ - "99.58947", - "99.58947", + "101.77517", + "102.35417", ], ], "startBinding": null, - "width": "99.58947", - "x": 0, "y": 0, }, "inserted": { "endBinding": { "elementId": "id171", "focus": "0.00990", - "gap": 5, + "gap": 1, }, - "height": "0.92929", + "height": "0.98586", "points": [ [ 0, 0, ], [ - "92.92893", - "-0.92929", + "98.58579", + "-0.98586", ], ], "startBinding": { "elementId": "id170", "focus": "0.02970", - "gap": 5, + "gap": 1, }, - "width": "92.92893", - "x": "3.53553", - "y": "0.96033", + "y": "0.99364", }, }, "id175" => Delta { @@ -864,7 +858,6 @@ History { 0, ], ], - "width": 0, }, "inserted": { "points": [ @@ -873,11 +866,10 @@ History { 0, ], [ - "85.85786", + 100, 0, ], ], - "width": "85.85786", }, }, }, @@ -934,14 +926,12 @@ History { ], ], "startBinding": null, - "width": 100, - "x": 150, }, "inserted": { "endBinding": { "elementId": "id166", "focus": -0, - "gap": 5, + "gap": 1, }, "points": [ [ @@ -956,10 +946,8 @@ History { "startBinding": { "elementId": "id165", "focus": 0, - "gap": 5, + "gap": 1, }, - "width": 0, - "x": "146.46447", }, }, }, @@ -1253,7 +1241,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.71911", + "height": "1.30038", "id": "id178", "index": "Zz", "isDeleted": false, @@ -1267,8 +1255,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "92.92893", - "1.71911", + "98.58579", + "1.30038", ], ], "roughness": 1, @@ -1291,8 +1279,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 11, - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -1625,7 +1613,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.71911", + "height": "1.30038", "id": "id181", "index": "a0", "isDeleted": false, @@ -1639,8 +1627,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "92.92893", - "1.71911", + "98.58579", + "1.30038", ], ], "roughness": 1, @@ -1663,8 +1651,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 11, - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -1783,7 +1771,7 @@ History { "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "12.86717", + "height": "11.27227", "index": "a0", "isDeleted": false, "lastCommittedPoint": null, @@ -1796,8 +1784,8 @@ History { 0, ], [ - "92.92893", - "12.86717", + "98.58579", + "11.27227", ], ], "roughness": 1, @@ -1818,8 +1806,8 @@ History { "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, }, "inserted": { @@ -2333,12 +2321,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "endBinding": { "elementId": "id185", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "369.21589", + "height": "374.05754", "id": "id186", "index": "a2", "isDeleted": false, @@ -2352,8 +2340,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "496.84035", - "-369.21589", + "502.78936", + "-374.05754", ], ], "roughness": 1, @@ -2364,7 +2352,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "startBinding": { "elementId": "id184", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -2372,9 +2360,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 10, - "width": "496.84035", - "x": "2.18463", - "y": "-38.80748", + "width": "502.78936", + "x": "-0.83465", + "y": "-36.58211", } `; @@ -2493,7 +2481,7 @@ History { "endBinding": { "elementId": "id185", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -2523,7 +2511,7 @@ History { "startBinding": { "elementId": "id184", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -15173,7 +15161,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "endBinding": { "elementId": "id58", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -15192,7 +15180,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "92.92893", + "98.58579", 0, ], ], @@ -15204,7 +15192,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id56", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -15212,8 +15200,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -15544,7 +15532,7 @@ History { "endBinding": { "elementId": "id58", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -15574,7 +15562,7 @@ History { "startBinding": { "elementId": "id56", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -15871,7 +15859,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "endBinding": { "elementId": "id52", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -15890,7 +15878,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "92.92893", + "98.58579", 0, ], ], @@ -15902,7 +15890,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id50", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -15910,8 +15898,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -16164,7 +16152,7 @@ History { "endBinding": { "elementId": "id52", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -16194,7 +16182,7 @@ History { "startBinding": { "elementId": "id50", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -16491,7 +16479,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "endBinding": { "elementId": "id64", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -16510,7 +16498,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "92.92893", + "98.58579", 0, ], ], @@ -16522,7 +16510,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id62", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -16530,8 +16518,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -16784,7 +16772,7 @@ History { "endBinding": { "elementId": "id64", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -16814,7 +16802,7 @@ History { "startBinding": { "elementId": "id62", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -17109,7 +17097,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "endBinding": { "elementId": "id70", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -17128,7 +17116,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "92.92893", + "98.58579", 0, ], ], @@ -17140,7 +17128,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id68", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -17148,8 +17136,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -17212,7 +17200,7 @@ History { "startBinding": { "elementId": "id68", "focus": 0, - "gap": 5, + "gap": 1, }, }, "inserted": { @@ -17472,7 +17460,7 @@ History { "endBinding": { "elementId": "id70", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -17502,7 +17490,7 @@ History { "startBinding": { "elementId": "id68", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -17823,7 +17811,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "endBinding": { "elementId": "id77", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -17842,7 +17830,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "92.92893", + "98.58579", 0, ], ], @@ -17854,7 +17842,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "startBinding": { "elementId": "id75", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -17862,8 +17850,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 11, - "width": "92.92893", - "x": "3.53553", + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -17925,7 +17913,7 @@ History { "endBinding": { "elementId": "id77", "focus": -0, - "gap": 5, + "gap": 1, }, "points": [ [ @@ -17940,7 +17928,7 @@ History { "startBinding": { "elementId": "id75", "focus": 0, - "gap": 5, + "gap": 1, }, }, "inserted": { @@ -18201,7 +18189,7 @@ History { "endBinding": { "elementId": "id77", "focus": -0, - "gap": 5, + "gap": 1, }, "fillStyle": "solid", "frameId": null, @@ -18231,7 +18219,7 @@ History { "startBinding": { "elementId": "id75", "focus": 0, - "gap": 5, + "gap": 1, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index eaea9d8908..4b863d4e78 100644 --- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -191,12 +191,12 @@ exports[`move element > rectangles with binding arrow 7`] = ` "endBinding": { "elementId": "id1", "focus": "-0.46667", - "gap": 5, + "gap": 10, }, "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "94.40997", + "height": "87.29887", "id": "id2", "index": "a2", "isDeleted": false, @@ -210,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` 0, ], [ - "93.92893", - "94.40997", + "86.85786", + "87.29887", ], ], "roughness": 1, @@ -223,7 +223,7 @@ exports[`move element > rectangles with binding arrow 7`] = ` "startBinding": { "elementId": "id0", "focus": "-0.60000", - "gap": 5, + "gap": 10, }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` "updated": 1, "version": 11, "versionNonce": 1051383431, - "width": "93.92893", - "x": "103.53553", - "y": "43.53553", + "width": "86.85786", + "x": "107.07107", + "y": "47.07107", } `; diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 6a51daf1f4..8dd65c7a5f 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -4779,12 +4779,12 @@ describe("history", () => { startBinding: expect.objectContaining({ elementId: rect1.id, focus: 0, - gap: FIXED_BINDING_DISTANCE, + gap: 1, }), endBinding: expect.objectContaining({ elementId: rect2.id, focus: -0, - gap: FIXED_BINDING_DISTANCE, + gap: 1, }), isDeleted: true, }), diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx index 0a9b7503a8..8619985846 100644 --- a/packages/excalidraw/tests/linearElementEditor.test.tsx +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -1266,7 +1266,7 @@ describe("Test Linear Elements", () => { mouse.downAt(rect.x, rect.y); mouse.moveTo(200, 0); mouse.upAt(200, 0); - expect(arrow.width).toBeCloseTo(206.86, 0); + expect(arrow.width).toBeCloseTo(204, 0); expect(rect.x).toBe(200); expect(rect.y).toBe(0); expect(handleBindTextResizeSpy).toHaveBeenCalledWith( diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index 42b17940a9..77fc7e57db 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -128,10 +128,8 @@ describe("move element", () => { expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectB.x, rectB.y]).toEqual([201, 2]); - expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[103.54, 43.53]]); - expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([ - [93.93, 94.41], - ]); + expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]); + expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]); h.elements.forEach((element) => expect(element).toMatchSnapshot()); }); diff --git a/packages/excalidraw/tests/rotate.test.tsx b/packages/excalidraw/tests/rotate.test.tsx index d3809d5947..9687b08f25 100644 --- a/packages/excalidraw/tests/rotate.test.tsx +++ b/packages/excalidraw/tests/rotate.test.tsx @@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async () expect(arrow.endBinding?.elementId).toEqual(rectangle.id); expect(arrow.x).toBeCloseTo(-80); expect(arrow.y).toBeCloseTo(50); - expect(arrow.width).toBeCloseTo(119.58, 1); + expect(arrow.width).toBeCloseTo(116.7, 1); expect(arrow.height).toBeCloseTo(0); }); @@ -72,13 +72,13 @@ test("unselected bound arrows update when rotating their target elements", async expect(ellipseArrow.x).toEqual(0); expect(ellipseArrow.y).toEqual(0); expect(ellipseArrow.points[0]).toEqual([0, 0]); - expect(ellipseArrow.points[1][0]).toBeCloseTo(54.36, 1); - expect(ellipseArrow.points[1][1]).toBeCloseTo(139.61, 1); + expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1); + expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1); expect(textArrow.endBinding?.elementId).toEqual(text.id); expect(textArrow.x).toEqual(360); expect(textArrow.y).toEqual(300); expect(textArrow.points[0]).toEqual([0, 0]); - expect(textArrow.points[1][0]).toBeCloseTo(-100.12, 0); - expect(textArrow.points[1][1]).toBeCloseTo(-123.63, 0); + expect(textArrow.points[1][0]).toBeCloseTo(-94, 0); + expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0); }); diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index a0b2e75c5c..a79fb43a19 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -157,13 +157,22 @@ export function curveIntersectLineSegment< return bezierEquation(c, t); }; - const solutions = [ - calculate(initial_guesses[0]), - calculate(initial_guesses[1]), - calculate(initial_guesses[2]), - ].filter((x, i, a): x is Point => x !== null && a.indexOf(x) === i); + let solution = calculate(initial_guesses[0]); + if (solution) { + return [solution]; + } - return solutions; + solution = calculate(initial_guesses[1]); + if (solution) { + return [solution]; + } + + solution = calculate(initial_guesses[2]); + if (solution) { + return [solution]; + } + + return []; } /** diff --git a/packages/math/src/point.ts b/packages/math/src/point.ts index bedffbf037..b6054a10a3 100644 --- a/packages/math/src/point.ts +++ b/packages/math/src/point.ts @@ -91,10 +91,9 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint { export function pointsEqual( a: Point, b: Point, - precision = PRECISION, ): boolean { const abs = Math.abs; - return abs(a[0] - b[0]) < precision && abs(a[1] - b[1]) < precision; + return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION; } /** diff --git a/packages/math/src/utils.ts b/packages/math/src/utils.ts index 89765fa670..8807c275e4 100644 --- a/packages/math/src/utils.ts +++ b/packages/math/src/utils.ts @@ -6,7 +6,7 @@ export const clamp = (value: number, min: number, max: number) => { export const round = ( value: number, - precision: number = (Math.log(1 / PRECISION) * Math.LOG10E + 1) | 0, + precision: number, func: "round" | "floor" | "ceil" = "round", ) => { const multiplier = Math.pow(10, precision);