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 {
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<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 = (
arrow: NonDeleted<ExcalidrawArrowElement>,
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 };
}

View File

@@ -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<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;
};