fix: Alt precise positioning

This commit is contained in:
Mark Tolmacs
2025-11-07 11:11:55 +01:00
parent 11bb0860ea
commit 6544bc9e3c
2 changed files with 172 additions and 148 deletions

View File

@@ -1,26 +1,24 @@
import { import {
KEYS, KEYS,
arrayToMap, arrayToMap,
debugDrawLine,
getFeatureFlag, getFeatureFlag,
invariant, invariant,
isTransparent, isTransparent,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
lineSegment,
pointFrom,
pointRotateRads,
type GlobalPoint,
vectorFromPoint,
pointDistanceSq,
clamp,
pointDistance,
pointFromVector,
vectorScale,
vectorNormalize,
PRECISION, PRECISION,
lineSegmentIntersectionPoints, clamp,
lineSegment,
pointDistance,
pointDistanceSq,
pointFrom,
pointFromVector,
pointRotateRads,
vectorFromPoint,
vectorNormalize,
vectorScale,
type GlobalPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math"; import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math";
@@ -56,33 +54,33 @@ import {
isBindableElement, isBindableElement,
isBoundToContainer, isBoundToContainer,
isElbowArrow, isElbowArrow,
isRectangularElement,
isRectanguloidElement, isRectanguloidElement,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { aabbForElement, elementCenterPoint } from "./bounds"; import { aabbForElement, elementCenterPoint } from "./bounds";
import { updateElbowArrowPoints } from "./elbowArrow"; import { updateElbowArrowPoints } from "./elbowArrow";
import { projectFixedPointOntoDiagonal } from "./utils";
import type { Scene } from "./Scene"; import type { Scene } from "./Scene";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
import type { ElementUpdate } from "./mutateElement"; import type { ElementUpdate } from "./mutateElement";
import type { import type {
ExcalidrawBindableElement, BindMode,
ExcalidrawElement,
NonDeleted,
NonDeletedExcalidrawElement,
ElementsMap, ElementsMap,
NonDeletedSceneElementsMap,
ExcalidrawTextElement,
ExcalidrawArrowElement, ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
ExcalidrawElement,
ExcalidrawTextElement,
FixedPoint, FixedPoint,
FixedPointBinding, FixedPointBinding,
PointsPositionUpdates, NonDeleted,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
Ordered, Ordered,
BindMode, PointsPositionUpdates,
} from "./types"; } from "./types";
export type BindingStrategy = export type BindingStrategy =
@@ -423,7 +421,7 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
invariant(false, "New arrow creation should not reach here"); invariant(false, "New arrow creation should not reach here");
}; };
const bindingStrategyForSimpleArrowEndpointDragging = ( const bindingStrategyForSimpleArrowEndpointDragging_complex = (
point: GlobalPoint, point: GlobalPoint,
currentBinding: FixedPointBinding | null, currentBinding: FixedPointBinding | null,
oppositeBinding: FixedPointBinding | null, oppositeBinding: FixedPointBinding | null,
@@ -689,7 +687,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
focusPoint: globalPoint, focusPoint: globalPoint,
} }
: { : {
mode: "orbit", mode: opts?.altKey ? "inside" : "orbit",
element: hit, element: hit,
focusPoint: opts?.altKey focusPoint: opts?.altKey
? globalPoint ? 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<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x, element.y),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(
element.x + element.width,
element.y + element.height,
),
center,
element.angle,
),
)
: lineSegment<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height,
),
center,
element.angle,
),
);
const diagonalTwo = isRectangularElement(element)
? lineSegment<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x + element.width, element.y),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(element.x, element.y + element.height),
center,
element.angle,
),
)
: lineSegment<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(
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<GlobalPoint>(
vectorScale(
vectorFromPoint(point, a),
2 * pointDistance(a, point) +
Math.max(
pointDistance(diagonalOne[0], diagonalOne[1]),
pointDistance(diagonalTwo[0], diagonalTwo[1]),
),
),
a,
);
const intersector = lineSegment<GlobalPoint>(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 = ( const getBindingStrategyForDraggingBindingElementEndpoints_complex = (
arrow: NonDeleted<ExcalidrawArrowElement>, arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates, draggingPoints: PointsPositionUpdates,
@@ -926,16 +821,17 @@ const getBindingStrategyForDraggingBindingElementEndpoints_complex = (
elementsMap, elementsMap,
); );
const { current, other } = bindingStrategyForSimpleArrowEndpointDragging( const { current, other } =
globalPoint, bindingStrategyForSimpleArrowEndpointDragging_complex(
arrow.startBinding, globalPoint,
arrow.endBinding, arrow.startBinding,
elementsMap, arrow.endBinding,
elements, elementsMap,
globalBindMode, elements,
arrow, globalBindMode,
opts?.finalize, arrow,
); opts?.finalize,
);
return { start: current, end: other }; return { start: current, end: other };
} }
@@ -949,16 +845,17 @@ const getBindingStrategyForDraggingBindingElementEndpoints_complex = (
localPoint, localPoint,
elementsMap, elementsMap,
); );
const { current, other } = bindingStrategyForSimpleArrowEndpointDragging( const { current, other } =
globalPoint, bindingStrategyForSimpleArrowEndpointDragging_complex(
arrow.endBinding, globalPoint,
arrow.startBinding, arrow.endBinding,
elementsMap, arrow.startBinding,
elements, elementsMap,
globalBindMode, elements,
arrow, globalBindMode,
opts?.finalize, arrow,
); opts?.finalize,
);
return { start: other, end: current }; return { start: other, end: current };
} }

View File

@@ -1,6 +1,8 @@
import { import {
debugDrawLine,
DEFAULT_ADAPTIVE_RADIUS, DEFAULT_ADAPTIVE_RADIUS,
DEFAULT_PROPORTIONAL_RADIUS, DEFAULT_PROPORTIONAL_RADIUS,
invariant,
LINE_CONFIRM_THRESHOLD, LINE_CONFIRM_THRESHOLD,
ROUNDNESS, ROUNDNESS,
} from "@excalidraw/common"; } from "@excalidraw/common";
@@ -10,10 +12,15 @@ import {
curveCatmullRomCubicApproxPoints, curveCatmullRomCubicApproxPoints,
curveOffsetPoints, curveOffsetPoints,
lineSegment, lineSegment,
lineSegmentIntersectionPoints,
pointDistance, pointDistance,
pointFrom, pointFrom,
pointFromArray, pointFromArray,
pointFromVector,
pointRotateRads,
rectangle, rectangle,
vectorFromPoint,
vectorScale,
type GlobalPoint, type GlobalPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
@@ -21,11 +28,16 @@ import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types"; import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
import { getDiamondPoints } from "./bounds"; import { elementCenterPoint, getDiamondPoints } from "./bounds";
import { generateLinearCollisionShape } from "./shape"; import { generateLinearCollisionShape } from "./shape";
import { LinearElementEditor } from "./linearElementEditor";
import { isRectangularElement } from "./typeChecks";
import type { import type {
ElementsMap,
ExcalidrawArrowElement,
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
@@ -471,3 +483,118 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
return 0; return 0;
}; };
const getDiagonalsForBindableElement = (
element: ExcalidrawElement,
elementsMap: ElementsMap,
) => {
const center = elementCenterPoint(element, elementsMap);
const diagonalOne = isRectangularElement(element)
? lineSegment<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x, element.y),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(
element.x + element.width,
element.y + element.height,
),
center,
element.angle,
),
)
: lineSegment<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height,
),
center,
element.angle,
),
);
const diagonalTwo = isRectangularElement(element)
? lineSegment<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x + element.width, element.y),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(element.x, element.y + element.height),
center,
element.angle,
),
)
: lineSegment<GlobalPoint>(
pointRotateRads(
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
center,
element.angle,
),
pointRotateRads(
pointFrom<GlobalPoint>(
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<GlobalPoint>(
vectorScale(
vectorFromPoint(point, a),
2 * pointDistance(a, point) +
Math.max(
pointDistance(diagonalOne[0], diagonalOne[1]),
pointDistance(diagonalTwo[0], diagonalTwo[1]),
),
),
a,
);
const intersector = lineSegment<GlobalPoint>(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;
};