diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 53354fdf0e..ab69a01fb6 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -1,26 +1,24 @@ import { KEYS, arrayToMap, - debugDrawLine, getFeatureFlag, invariant, isTransparent, } from "@excalidraw/common"; import { - lineSegment, - pointFrom, - pointRotateRads, - type GlobalPoint, - vectorFromPoint, - pointDistanceSq, - clamp, - pointDistance, - pointFromVector, - vectorScale, - vectorNormalize, PRECISION, - lineSegmentIntersectionPoints, + clamp, + lineSegment, + pointDistance, + pointDistanceSq, + pointFrom, + pointFromVector, + pointRotateRads, + vectorFromPoint, + vectorNormalize, + vectorScale, + type GlobalPoint, } from "@excalidraw/math"; import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math"; @@ -56,33 +54,33 @@ import { isBindableElement, isBoundToContainer, isElbowArrow, - isRectangularElement, isRectanguloidElement, isTextElement, } from "./typeChecks"; import { aabbForElement, elementCenterPoint } from "./bounds"; import { updateElbowArrowPoints } from "./elbowArrow"; +import { projectFixedPointOntoDiagonal } from "./utils"; import type { Scene } from "./Scene"; import type { Bounds } from "./bounds"; import type { ElementUpdate } from "./mutateElement"; import type { - ExcalidrawBindableElement, - ExcalidrawElement, - NonDeleted, - NonDeletedExcalidrawElement, + BindMode, ElementsMap, - NonDeletedSceneElementsMap, - ExcalidrawTextElement, ExcalidrawArrowElement, + ExcalidrawBindableElement, ExcalidrawElbowArrowElement, + ExcalidrawElement, + ExcalidrawTextElement, FixedPoint, FixedPointBinding, - PointsPositionUpdates, + NonDeleted, + NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, Ordered, - BindMode, + PointsPositionUpdates, } from "./types"; export type BindingStrategy = @@ -423,7 +421,7 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( invariant(false, "New arrow creation should not reach here"); }; -const bindingStrategyForSimpleArrowEndpointDragging = ( +const bindingStrategyForSimpleArrowEndpointDragging_complex = ( point: GlobalPoint, currentBinding: FixedPointBinding | null, oppositeBinding: FixedPointBinding | null, @@ -689,7 +687,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( focusPoint: globalPoint, } : { - mode: "orbit", + mode: opts?.altKey ? "inside" : "orbit", element: hit, focusPoint: opts?.altKey ? globalPoint @@ -738,109 +736,6 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = ( }; }; -const projectFixedPointOntoDiagonal = ( - arrow: ExcalidrawArrowElement, - point: GlobalPoint, - element: ExcalidrawElement, - startOrEnd: "start" | "end", - elementsMap: ElementsMap, -) => { - const center = elementCenterPoint(element, elementsMap); - const diagonalOne = isRectangularElement(element) - ? lineSegment( - pointRotateRads( - pointFrom(element.x, element.y), - center, - element.angle, - ), - pointRotateRads( - pointFrom( - element.x + element.width, - element.y + element.height, - ), - center, - element.angle, - ), - ) - : lineSegment( - pointRotateRads( - pointFrom(element.x + element.width / 2, element.y), - center, - element.angle, - ), - pointRotateRads( - pointFrom( - element.x + element.width / 2, - element.y + element.height, - ), - center, - element.angle, - ), - ); - const diagonalTwo = isRectangularElement(element) - ? lineSegment( - pointRotateRads( - pointFrom(element.x + element.width, element.y), - center, - element.angle, - ), - pointRotateRads( - pointFrom(element.x, element.y + element.height), - center, - element.angle, - ), - ) - : lineSegment( - pointRotateRads( - pointFrom(element.x, element.y + element.height / 2), - center, - element.angle, - ), - pointRotateRads( - pointFrom( - element.x + element.width, - element.y + element.height / 2, - ), - center, - element.angle, - ), - ); - - invariant(arrow.points.length >= 2, "Arrow must have at least two points"); - - const a = LinearElementEditor.getPointAtIndexGlobalCoordinates( - arrow, - startOrEnd === "start" ? 1 : arrow.points.length - 2, - elementsMap, - ); - const b = pointFromVector( - vectorScale( - vectorFromPoint(point, a), - 2 * pointDistance(a, point) + - Math.max( - pointDistance(diagonalOne[0], diagonalOne[1]), - pointDistance(diagonalTwo[0], diagonalTwo[1]), - ), - ), - a, - ); - const intersector = lineSegment(a, b); - const p1 = lineSegmentIntersectionPoints(diagonalOne, intersector); - const p2 = lineSegmentIntersectionPoints(diagonalTwo, intersector); - const d1 = p1 && pointDistance(a, p1); - const d2 = p2 && pointDistance(a, p2); - - debugDrawLine(diagonalOne, { color: "purple" }); - debugDrawLine(diagonalTwo, { color: "purple" }); - debugDrawLine(intersector, { color: "orange" }); - - if (d1 != null && d2 != null) { - return d1 < d2 ? p1 : p2; - } - - return p1 || p2 || null; -}; - const getBindingStrategyForDraggingBindingElementEndpoints_complex = ( arrow: NonDeleted, draggingPoints: PointsPositionUpdates, @@ -926,16 +821,17 @@ const getBindingStrategyForDraggingBindingElementEndpoints_complex = ( elementsMap, ); - const { current, other } = bindingStrategyForSimpleArrowEndpointDragging( - globalPoint, - arrow.startBinding, - arrow.endBinding, - elementsMap, - elements, - globalBindMode, - arrow, - opts?.finalize, - ); + const { current, other } = + bindingStrategyForSimpleArrowEndpointDragging_complex( + globalPoint, + arrow.startBinding, + arrow.endBinding, + elementsMap, + elements, + globalBindMode, + arrow, + opts?.finalize, + ); return { start: current, end: other }; } @@ -949,16 +845,17 @@ const getBindingStrategyForDraggingBindingElementEndpoints_complex = ( localPoint, elementsMap, ); - const { current, other } = bindingStrategyForSimpleArrowEndpointDragging( - globalPoint, - arrow.endBinding, - arrow.startBinding, - elementsMap, - elements, - globalBindMode, - arrow, - opts?.finalize, - ); + const { current, other } = + bindingStrategyForSimpleArrowEndpointDragging_complex( + globalPoint, + arrow.endBinding, + arrow.startBinding, + elementsMap, + elements, + globalBindMode, + arrow, + opts?.finalize, + ); return { start: other, end: current }; } diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index a352db61c5..7483101f7e 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -1,6 +1,8 @@ import { + debugDrawLine, DEFAULT_ADAPTIVE_RADIUS, DEFAULT_PROPORTIONAL_RADIUS, + invariant, LINE_CONFIRM_THRESHOLD, ROUNDNESS, } from "@excalidraw/common"; @@ -10,10 +12,15 @@ import { curveCatmullRomCubicApproxPoints, curveOffsetPoints, lineSegment, + lineSegmentIntersectionPoints, pointDistance, pointFrom, pointFromArray, + pointFromVector, + pointRotateRads, rectangle, + vectorFromPoint, + vectorScale, type GlobalPoint, } from "@excalidraw/math"; @@ -21,11 +28,16 @@ import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math"; import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types"; -import { getDiamondPoints } from "./bounds"; +import { elementCenterPoint, getDiamondPoints } from "./bounds"; import { generateLinearCollisionShape } from "./shape"; +import { LinearElementEditor } from "./linearElementEditor"; +import { isRectangularElement } from "./typeChecks"; + import type { + ElementsMap, + ExcalidrawArrowElement, ExcalidrawDiamondElement, ExcalidrawElement, ExcalidrawFreeDrawElement, @@ -471,3 +483,118 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => { return 0; }; + +const getDiagonalsForBindableElement = ( + element: ExcalidrawElement, + elementsMap: ElementsMap, +) => { + const center = elementCenterPoint(element, elementsMap); + const diagonalOne = isRectangularElement(element) + ? lineSegment( + pointRotateRads( + pointFrom(element.x, element.y), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + element.x + element.width, + element.y + element.height, + ), + center, + element.angle, + ), + ) + : lineSegment( + pointRotateRads( + pointFrom(element.x + element.width / 2, element.y), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + element.x + element.width / 2, + element.y + element.height, + ), + center, + element.angle, + ), + ); + const diagonalTwo = isRectangularElement(element) + ? lineSegment( + pointRotateRads( + pointFrom(element.x + element.width, element.y), + center, + element.angle, + ), + pointRotateRads( + pointFrom(element.x, element.y + element.height), + center, + element.angle, + ), + ) + : lineSegment( + pointRotateRads( + pointFrom(element.x, element.y + element.height / 2), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + element.x + element.width, + element.y + element.height / 2, + ), + center, + element.angle, + ), + ); + + return [diagonalOne, diagonalTwo]; +}; + +export const projectFixedPointOntoDiagonal = ( + arrow: ExcalidrawArrowElement, + point: GlobalPoint, + element: ExcalidrawElement, + startOrEnd: "start" | "end", + elementsMap: ElementsMap, +) => { + const [diagonalOne, diagonalTwo] = getDiagonalsForBindableElement( + element, + elementsMap, + ); + + invariant(arrow.points.length >= 2, "Arrow must have at least two points"); + + const a = LinearElementEditor.getPointAtIndexGlobalCoordinates( + arrow, + startOrEnd === "start" ? 1 : arrow.points.length - 2, + elementsMap, + ); + const b = pointFromVector( + vectorScale( + vectorFromPoint(point, a), + 2 * pointDistance(a, point) + + Math.max( + pointDistance(diagonalOne[0], diagonalOne[1]), + pointDistance(diagonalTwo[0], diagonalTwo[1]), + ), + ), + a, + ); + const intersector = lineSegment(a, b); + const p1 = lineSegmentIntersectionPoints(diagonalOne, intersector); + const p2 = lineSegmentIntersectionPoints(diagonalTwo, intersector); + const d1 = p1 && pointDistance(a, p1); + const d2 = p2 && pointDistance(a, p2); + + debugDrawLine(diagonalOne, { color: "purple" }); + debugDrawLine(diagonalTwo, { color: "purple" }); + debugDrawLine(intersector, { color: "orange" }); + + if (d1 != null && d2 != null) { + return d1 < d2 ? p1 : p2; + } + + return p1 || p2 || null; +};