Restore master

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs 2025-05-01 09:44:02 +02:00
parent 43561b6631
commit f6203daac5
No known key found for this signature in database
7 changed files with 93 additions and 252 deletions

View file

@ -27,9 +27,7 @@ import {
PRECISION,
} from "@excalidraw/math";
import { isPointInShape } from "@excalidraw/utils/collision";
import { getEllipseShape, getPolygonShape } from "@excalidraw/utils/shape";
import { isPointOnShape } from "@excalidraw/utils/collision";
import type { LocalPoint, Radians } from "@excalidraw/math";
@ -46,6 +44,7 @@ import { intersectElementWithLineSegment } from "./collision";
import { distanceToBindableElement } from "./distance";
import {
headingForPointFromElement,
headingIsHorizontal,
vectorToHeading,
type Heading,
} from "./heading";
@ -64,7 +63,7 @@ import {
isTextElement,
} from "./typeChecks";
import { aabbForElement } from "./shapes";
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
import { updateElbowArrowPoints } from "./elbowArrow";
import type Scene from "./Scene";
@ -108,7 +107,7 @@ export const isBindingEnabled = (appState: AppState): boolean => {
};
export const FIXED_BINDING_DISTANCE = 5;
const BINDING_HIGHLIGHT_THICKNESS = 10;
export const BINDING_HIGHLIGHT_THICKNESS = 10;
export const BINDING_HIGHLIGHT_OFFSET = 4;
const getNonDeletedElements = (
@ -442,15 +441,19 @@ 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: Math.min(binding.gap, maxGap),
gap,
};
};
@ -562,25 +565,21 @@ export const getHoveredElementForBinding = (
let cullRest = false;
const candidateElements = getAllElementsAtPositionForBinding(
elements,
(element) => {
const result =
isBindableElement(element, false) &&
bindingBorderTest(
element,
pointerCoords,
elementsMap,
zoom,
(fullShape ||
!isBindingFallthroughEnabled(
element as ExcalidrawBindableElement,
)) &&
// disable fullshape snapping for frame elements so we
// can bind to frame children
!isFrameLikeElement(element),
);
return result;
},
(element) =>
isBindableElement(element, false) &&
bindingBorderTest(
element,
pointerCoords,
elementsMap,
zoom,
(fullShape ||
!isBindingFallthroughEnabled(
element as ExcalidrawBindableElement,
)) &&
// disable fullshape snapping for frame elements so we
// can bind to frame children
!isFrameLikeElement(element),
),
).filter((element) => {
if (cullRest) {
return false;
@ -888,12 +887,7 @@ export const getHeadingForElbowArrowSnap = (
return otherPointHeading;
}
const distance = getDistanceForBinding(
origPoint,
bindableElement,
true,
zoom,
);
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
if (!distance) {
return vectorToHeading(
@ -904,10 +898,9 @@ export const getHeadingForElbowArrowSnap = (
return headingForPointFromElement(bindableElement, aabb, p);
};
export const getDistanceForBinding = (
const getDistanceForBinding = (
point: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement,
fullShape: boolean,
zoom?: AppState["zoom"],
) => {
const distance = distanceToBindableElement(bindableElement, point);
@ -917,16 +910,8 @@ export const getDistanceForBinding = (
bindableElement.height,
zoom,
);
const isInside = fullShape
? isPointInShape(
point,
bindableElement.type === "ellipse"
? getEllipseShape(bindableElement)
: getPolygonShape(bindableElement),
)
: false;
return distance > bindDistance && !isInside ? null : distance;
return distance > bindDistance ? null : distance;
};
export const bindPointToSnapToElementOutline = (
@ -962,16 +947,23 @@ export const bindPointToSnapToElementOutline = (
let intersection: GlobalPoint | null = null;
if (elbowed) {
const isHorizontal = headingIsHorizontal(
headingForPointFromElement(bindableElement, aabb, globalP),
);
const otherPoint = pointFrom<GlobalPoint>(
isHorizontal ? center[0] : edgePoint[0],
!isHorizontal ? center[1] : edgePoint[1],
);
intersection = intersectElementWithLineSegment(
bindableElement,
lineSegment(
center,
otherPoint,
pointFromVector(
vectorScale(
vectorNormalize(vectorFromPoint(edgePoint, center)),
vectorNormalize(vectorFromPoint(edgePoint, otherPoint)),
Math.max(bindableElement.width, bindableElement.height) * 2,
),
center,
otherPoint,
),
),
)[0];
@ -1179,48 +1171,6 @@ export const snapToMid = (
center,
angle,
);
} else if (element.type === "diamond") {
const distance = FIXED_BINDING_DISTANCE - 1;
const topLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + height / 4 - distance,
);
const topRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + height / 4 - distance,
);
const bottomLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + (3 * height) / 4 + distance,
);
const bottomRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + (3 * height) / 4 + distance,
);
if (
pointDistance(topLeft, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(topLeft, center, angle);
}
if (
pointDistance(topRight, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(topRight, center, angle);
}
if (
pointDistance(bottomLeft, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(bottomLeft, center, angle);
}
if (
pointDistance(bottomRight, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(bottomRight, center, angle);
}
}
return p;
@ -1561,14 +1511,14 @@ export const bindingBorderTest = (
zoom?: AppState["zoom"],
fullShape?: boolean,
): boolean => {
const distance = getDistanceForBinding(
pointFrom(x, y),
element,
!!fullShape,
zoom,
);
const threshold = maxBindingGap(element, element.width, element.height, zoom);
return !!distance;
const shape = getElementShape(element, elementsMap);
return (
isPointOnShape(pointFrom(x, y), shape, threshold) ||
(fullShape === true &&
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
);
};
export const maxBindingGap = (

View file

@ -31,7 +31,6 @@ import {
getGlobalFixedPointForBindableElement,
snapToMid,
getHoveredElementForBinding,
getDistanceForBinding,
} from "./binding";
import { distanceToBindableElement } from "./distance";
import {
@ -1256,7 +1255,6 @@ const getElbowArrowData = (
origStartGlobalPoint,
hoveredStartElement,
options?.isDragging,
options?.zoom,
);
const endGlobalPoint = getGlobalPoint(
{
@ -1270,7 +1268,6 @@ const getElbowArrowData = (
origEndGlobalPoint,
hoveredEndElement,
options?.isDragging,
options?.zoom,
);
const startHeading = getBindPointHeading(
startGlobalPoint,
@ -2214,14 +2211,16 @@ const getGlobalPoint = (
initialPoint: GlobalPoint,
element?: ExcalidrawBindableElement | null,
isDragging?: boolean,
zoom?: AppState["zoom"],
): GlobalPoint => {
if (isDragging) {
if (element && getDistanceForBinding(initialPoint, element, true, zoom)) {
return snapToMid(
if (element) {
const snapPoint = bindPointToSnapToElementOutline(
arrow,
element,
bindPointToSnapToElementOutline(arrow, element, startOrEnd),
startOrEnd,
);
return snapToMid(element, snapPoint);
}
return initialPoint;

View file

@ -3,12 +3,10 @@ import {
viewportCoordsToSceneCoords,
} from "@excalidraw/common";
import { pointsEqual } from "@excalidraw/math";
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
import { getCommonBounds, getElementBounds } from "./bounds";
import { isElbowArrow, isFreeDrawElement, isLinearElement } from "./typeChecks";
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
import type { ElementsMap, ExcalidrawElement } from "./types";
@ -18,12 +16,6 @@ import type { ElementsMap, ExcalidrawElement } from "./types";
export const isInvisiblySmallElement = (
element: ExcalidrawElement,
): boolean => {
if (isElbowArrow(element)) {
return (
element.points.length < 2 ||
pointsEqual(element.points[0], element.points[element.points.length - 1])
);
}
if (isLinearElement(element) || isFreeDrawElement(element)) {
return element.points.length < 2;
}

View file

@ -199,7 +199,7 @@ describe("elbow arrow routing", () => {
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
});
expect(arrow.points).toCloselyEqualPoints([
expect(arrow.points).toEqual([
[0, 0],
[45, 0],
[45, 200],
@ -253,7 +253,7 @@ describe("elbow arrow ui", () => {
expect(arrow.type).toBe("arrow");
expect(arrow.elbowed).toBe(true);
expect(arrow.points).toCloselyEqualPoints([
expect(arrow.points).toEqual([
[0, 0],
[45, 0],
[45, 200],
@ -351,7 +351,7 @@ describe("elbow arrow ui", () => {
expect(duplicatedArrow.id).not.toBe(originalArrowId);
expect(duplicatedArrow.type).toBe("arrow");
expect(duplicatedArrow.elbowed).toBe(true);
expect(duplicatedArrow.points).toCloselyEqualPoints([
expect(duplicatedArrow.points).toEqual([
[0, 0],
[45, 0],
[45, 200],
@ -405,98 +405,11 @@ describe("elbow arrow ui", () => {
expect(duplicatedArrow.id).not.toBe(originalArrowId);
expect(duplicatedArrow.type).toBe("arrow");
expect(duplicatedArrow.elbowed).toBe(true);
expect(duplicatedArrow.points).toCloselyEqualPoints([
expect(duplicatedArrow.points).toEqual([
[0, 0],
[0, 100],
[90, 100],
[90, 200],
]);
});
it("elbow arrow snap at diamond quarter point too", async () => {
UI.createElement("diamond", {
x: -50,
y: -50,
width: 100,
height: 100,
});
UI.clickTool("arrow");
UI.clickOnTestId("elbow-arrow");
mouse.reset();
mouse.moveTo(43, 99);
mouse.click();
mouse.moveTo(27, 25);
mouse.click();
let arrow = h.scene.getSelectedElements(
h.state,
)[0] as ExcalidrawArrowElement;
expect(arrow.endBinding).not.toBe(null);
expect(arrow.x + arrow.points[arrow.points.length - 1][0]).toBeCloseTo(
29.0355,
);
expect(arrow.y + arrow.points[arrow.points.length - 1][1]).toBeCloseTo(
29.0355,
);
UI.clickTool("arrow");
UI.clickOnTestId("elbow-arrow");
mouse.reset();
mouse.moveTo(43, 99);
mouse.click();
mouse.moveTo(-23, 25);
mouse.click();
arrow = h.scene.getSelectedElements(h.state)[0] as ExcalidrawArrowElement;
expect(arrow.endBinding).not.toBe(null);
expect(arrow.x + arrow.points[arrow.points.length - 1][0]).toBeCloseTo(
-28.5559,
);
expect(arrow.y + arrow.points[arrow.points.length - 1][1]).toBeCloseTo(
28.5559,
);
UI.clickTool("arrow");
UI.clickOnTestId("elbow-arrow");
mouse.reset();
mouse.moveTo(43, 99);
mouse.click();
mouse.moveTo(-27, -25);
mouse.click();
arrow = h.scene.getSelectedElements(h.state)[0] as ExcalidrawArrowElement;
expect(arrow.endBinding).not.toBe(null);
expect(arrow.x + arrow.points[arrow.points.length - 1][0]).toBeCloseTo(
-28.0355,
);
expect(arrow.y + arrow.points[arrow.points.length - 1][1]).toBeCloseTo(
-28.0355,
);
UI.clickTool("arrow");
UI.clickOnTestId("elbow-arrow");
mouse.reset();
mouse.moveTo(43, 99);
mouse.click();
mouse.moveTo(23, -25);
mouse.click();
arrow = h.scene.getSelectedElements(h.state)[0] as ExcalidrawArrowElement;
expect(arrow.endBinding).not.toBe(null);
expect(arrow.x + arrow.points[arrow.points.length - 1][0]).toBeCloseTo(
28.5559,
);
expect(arrow.y + arrow.points[arrow.points.length - 1][1]).toBeCloseTo(
-28.5559,
);
});
});