This commit is contained in:
Márk Tolmács 2025-04-13 21:22:20 +02:00 committed by GitHub
commit 97b6b46848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 104 additions and 41 deletions

View file

@ -18,7 +18,7 @@ import {
} from "@excalidraw/math";
import { isCurve } from "@excalidraw/math/curve";
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
import type { DebugElement } from "@excalidraw/utils/visualdebug";
import type { Curve } from "@excalidraw/math";

View file

@ -981,6 +981,7 @@ export const bindPointToSnapToElementOutline = (
otherPoint,
),
),
0.1,
)[0];
} else {
intersection = intersectElementWithLineSegment(

View file

@ -53,7 +53,7 @@ import {
type SceneElementsMap,
} from "./types";
import { aabbForElement, pointInsideBounds } from "./shapes";
import { aabbForElement, aabbForPoints, pointInsideBounds } from "./shapes";
import type { Bounds } from "./bounds";
import type { Heading } from "./heading";
@ -107,8 +107,34 @@ type ElbowArrowData = {
hoveredEndElement: ExcalidrawBindableElement | null;
};
const DEDUP_TRESHOLD = 1;
export const BASE_PADDING = 40;
const calculateDedupTreshhold = <Point extends GlobalPoint | LocalPoint>(
a: Point,
b: Point,
) => 1 + pointDistance(a, b) / 100;
const calculatePadding = (
aabb: Bounds,
startHeading: Heading,
endHeading: Heading,
) => {
const width = aabb[2] - aabb[0];
const height = aabb[3] - aabb[1];
const size = Math.max(width, height);
return size > 75
? 40
: Math.min(
Math.max(
headingIsHorizontal(startHeading) ? width / 2 - 1 : height / 2 - 1,
10,
),
Math.max(
headingIsHorizontal(endHeading) ? width / 2 - 1 : height / 2 - 1,
10,
),
40,
);
};
const handleSegmentRenormalization = (
arrow: ExcalidrawElbowArrowElement,
@ -184,7 +210,11 @@ const handleSegmentRenormalization = (
if (
// Remove segments that are too short
pointDistance(points[i - 2], points[i - 1]) < DEDUP_TRESHOLD
pointDistance(points[i - 2], points[i - 1]) <
calculateDedupTreshhold(
points[i - 3] ?? points[i - 3],
points[i] ?? points[i - 1],
)
) {
const prevPrevSegmentIdx =
nextFixedSegments?.findIndex((segment) => segment.index === i - 2) ??
@ -464,6 +494,11 @@ const handleSegmentMove = (
hoveredStartElement: ExcalidrawBindableElement | null,
hoveredEndElement: ExcalidrawBindableElement | null,
): ElementUpdate<ExcalidrawElbowArrowElement> => {
const BASE_PADDING = calculatePadding(
aabbForElement(arrow),
startHeading,
endHeading,
);
const activelyModifiedSegmentIdx = fixedSegments
.map((segment, i) => {
if (
@ -708,6 +743,11 @@ const handleEndpointDrag = (
hoveredStartElement: ExcalidrawBindableElement | null,
hoveredEndElement: ExcalidrawBindableElement | null,
) => {
const BASE_PADDING = calculatePadding(
aabbForPoints([startGlobalPoint, endGlobalPoint]),
startHeading,
endHeading,
);
let startIsSpecial = arrow.startIsSpecial ?? null;
let endIsSpecial = arrow.endIsSpecial ?? null;
const globalUpdatedPoints = updatedPoints.map((p, i) =>
@ -742,6 +782,7 @@ const handleEndpointDrag = (
// Calculate the moving second point connection and add the start point
{
startIsSpecial = arrow.startIsSpecial && globalUpdatedPoints.length > 2;
const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1];
const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2];
const startIsHorizontal = headingIsHorizontal(startHeading);
@ -802,6 +843,7 @@ const handleEndpointDrag = (
// Calculate the moving second to last point connection
{
endIsSpecial = arrow.endIsSpecial && globalUpdatedPoints.length > 2;
const secondToLastPoint =
globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)];
const thirdToLastPoint =
@ -1296,6 +1338,11 @@ const getElbowArrowData = (
endGlobalPoint[0] + 2,
endGlobalPoint[1] + 2,
] as Bounds;
const BASE_PADDING = calculatePadding(
aabbForPoints([startGlobalPoint, endGlobalPoint]),
startHeading,
endHeading,
);
const startElementBounds = hoveredStartElement
? aabbForElement(
hoveredStartElement,
@ -2188,7 +2235,10 @@ const removeElbowArrowShortSegments = (
const prev = points[idx - 1];
const prevDist = pointDistance(prev, p);
return prevDist > DEDUP_TRESHOLD;
return (
prevDist >
calculateDedupTreshhold(points[idx - 2] ?? prev, points[idx + 1] ?? p)
);
});
}
@ -2293,13 +2343,16 @@ const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
export const validateElbowPoints = <P extends GlobalPoint | LocalPoint>(
points: readonly P[],
tolerance: number = DEDUP_TRESHOLD,
tolerance?: number,
) =>
points
.slice(1)
.map(
(p, i) =>
Math.abs(p[0] - points[i][0]) < tolerance ||
Math.abs(p[1] - points[i][1]) < tolerance,
)
.map((p, i) => {
const t =
tolerance ??
calculateDedupTreshhold(points[i - 1] ?? points[i], points[i + 2] ?? p);
return (
Math.abs(p[0] - points[i][0]) < t || Math.abs(p[1] - points[i][1]) < t
);
})
.every(Boolean);

View file

@ -282,6 +282,15 @@ export const mapIntervalToBezierT = <P extends GlobalPoint | LocalPoint>(
);
};
export const aabbForPoints = <Point extends GlobalPoint | LocalPoint>(
points: Point[],
): Bounds => [
Math.min(...points.map((point) => point[0])),
Math.min(...points.map((point) => point[1])),
Math.max(...points.map((point) => point[0])),
Math.max(...points.map((point) => point[1])),
];
/**
* Get the axis-aligned bounding box for a given element
*/

View file

@ -77,9 +77,9 @@ describe("elbow arrow segment move", () => {
expect(arrow.points).toCloselyEqualPoints([
[0, 0],
[110, 0],
[110, 200],
[190, 200],
[109.92, 0],
[109.92, 200],
[189.85, 200],
]);
mouse.reset();
@ -88,9 +88,9 @@ describe("elbow arrow segment move", () => {
expect(arrow.points).toCloselyEqualPoints([
[0, 0],
[110, 0],
[110, 200],
[190, 200],
[109.92, 0],
[109.92, 200],
[189.85, 200],
]);
});
@ -198,11 +198,11 @@ describe("elbow arrow routing", () => {
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
});
expect(arrow.points).toEqual([
expect(arrow.points).toCloselyEqualPoints([
[0, 0],
[45, 0],
[45, 200],
[90, 200],
[44.92, 0],
[44.92, 200],
[89.85, 200],
]);
});
});
@ -252,11 +252,11 @@ describe("elbow arrow ui", () => {
expect(arrow.type).toBe("arrow");
expect(arrow.elbowed).toBe(true);
expect(arrow.points).toEqual([
expect(arrow.points).toCloselyEqualPoints([
[0, 0],
[45, 0],
[45, 200],
[90, 200],
[44.92, 0],
[44.92, 200],
[89.85, 200],
]);
});
@ -294,11 +294,11 @@ describe("elbow arrow ui", () => {
) as HTMLInputElement;
UI.updateInput(inputAngle, String("40"));
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
expect(arrow.points).toCloselyEqualPoints([
[0, 0],
[35, 0],
[35, 165],
[103, 165],
[34.9292, 0],
[34.48768, 164.6246],
[104.333, 164.6246],
]);
});
@ -350,11 +350,11 @@ describe("elbow arrow ui", () => {
expect(duplicatedArrow.id).not.toBe(originalArrowId);
expect(duplicatedArrow.type).toBe("arrow");
expect(duplicatedArrow.elbowed).toBe(true);
expect(duplicatedArrow.points).toEqual([
expect(duplicatedArrow.points).toCloselyEqualPoints([
[0, 0],
[45, 0],
[45, 200],
[90, 200],
[44.92, 0],
[44.92, 200],
[89.85, 200],
]);
expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null);
@ -404,11 +404,11 @@ describe("elbow arrow ui", () => {
expect(duplicatedArrow.id).not.toBe(originalArrowId);
expect(duplicatedArrow.type).toBe("arrow");
expect(duplicatedArrow.elbowed).toBe(true);
expect(duplicatedArrow.points).toEqual([
expect(duplicatedArrow.points).toCloselyEqualPoints([
[0, 0],
[0, 100],
[90, 100],
[90, 200],
[89.85, 100],
[89.85, 200],
]);
});
});

View file

@ -510,12 +510,12 @@ describe("arrow element", () => {
h.state,
)[0] as ExcalidrawElbowArrowElement;
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
UI.resize(rectangle, "se", [-200, -150]);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
});
@ -538,11 +538,11 @@ describe("arrow element", () => {
h.state,
)[0] as ExcalidrawElbowArrowElement;
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
UI.resize([rectangle, arrow], "nw", [300, 350]);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
});
});