Compare commits

...

29 Commits

Author SHA1 Message Date
Márk Tolmács
4eb1bd8036
Merge branch 'master' into mtolmacs/fix/small-elbow-routing 2025-05-05 09:56:19 +02:00
Márk Tolmács
8d7ffa21d1
Merge branch 'master' into mtolmacs/fix/small-elbow-routing 2025-05-02 09:47:18 +02:00
dwelle
82cef23c3d DEBUG 2025-04-30 10:39:54 +02:00
Mark Tolmacs
541725ff5a
Test fixes 2025-04-28 19:49:41 +02:00
Mark Tolmacs
28066034d7
Arrowhead padding 2025-04-28 19:37:50 +02:00
Mark Tolmacs
7d0d6aec7a
Another algo forpaddings 2025-04-28 19:31:30 +02:00
Mark Tolmacs
e6ade3b627
Fix test 2025-04-25 18:58:05 +02:00
Mark Tolmacs
9a2bd18904
Further adjustments for edge cases 2025-04-25 18:54:18 +02:00
Mark Tolmacs
c7c6a4c3f1
Fix test 2025-04-25 14:33:34 +02:00
Márk Tolmács
9c27f936de
Merge branch 'master' into mtolmacs/fix/small-elbow-routing 2025-04-25 14:27:15 +02:00
Mark Tolmacs
b8fdd7ef23
Remove unneeded imports 2025-04-25 14:26:01 +02:00
Mark Tolmacs
ece841326b
Refine corner avoidance 2025-04-25 14:25:40 +02:00
Mark Tolmacs
41711af210 Adjust padding so smaller objects have smaller padding 2025-04-21 17:24:13 +02:00
Mark Tolmacs
230e47fd52 Remove debug 2025-04-21 14:56:58 +02:00
Mark Tolmacs
52445aeb68 Fix a particular routing issue 2025-04-21 14:56:36 +02:00
Márk Tolmács
bc9f34e71e
Merge branch 'master' into mtolmacs/fix/small-elbow-routing 2025-04-21 12:30:49 +02:00
Márk Tolmács
22aade07b3
Merge branch 'master' into mtolmacs/fix/small-elbow-routing 2025-04-16 21:50:42 +02:00
Mark Tolmacs
c2de1304b7
Add snapshot update
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2025-04-15 16:13:01 +02:00
Mark Tolmacs
25fb43f5b7
Snapshot update
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2025-04-14 19:05:34 +02:00
Márk Tolmács
6dfa5de66c
Merge branch 'master' into mtolmacs/fix/small-elbow-routing 2025-04-14 18:54:01 +02:00
Mark Tolmacs
7abbb2afa3
New heuristic based on minimal arrow extent
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2025-04-14 18:52:51 +02:00
Mark Tolmacs
aa91a3d610 Adaptive segment unification 2025-04-13 13:52:24 +02:00
Mark Tolmacs
25d6e517c9 Revert attempt to exclude some files from coverage 2025-04-13 13:29:21 +02:00
Mark Tolmacs
d5e33730ab Reduce scope of coverage reports 2025-04-13 13:19:45 +02:00
Mark Tolmacs
c06b78c1b2 Further fine-tune adaptive padding 2025-04-13 13:08:22 +02:00
Mark Tolmacs
eaa869620e Fine tuning 2025-04-11 21:53:23 +02:00
Mark Tolmacs
a8338cdb5a More adaptive elbow dongle offset 2025-04-11 21:08:55 +02:00
Mark Tolmacs
1ee3676784 Move visal debug to @excalidraw/util 2025-04-11 15:39:31 +02:00
Mark Tolmacs
f12f7e4b50 Fix sentry#6530117915 2025-04-11 10:24:02 +02:00
8 changed files with 232 additions and 82 deletions

View File

@ -18,7 +18,7 @@ import {
} from "@excalidraw/math"; } from "@excalidraw/math";
import { isCurve } from "@excalidraw/math/curve"; 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"; import type { Curve } from "@excalidraw/math";

View File

@ -43,6 +43,12 @@ import {
import { intersectElementWithLineSegment } from "./collision"; import { intersectElementWithLineSegment } from "./collision";
import { distanceToBindableElement } from "./distance"; import { distanceToBindableElement } from "./distance";
import { import {
compareHeading,
HEADING_DOWN,
HEADING_LEFT,
HEADING_RIGHT,
HEADING_UP,
headingForPoint,
headingForPointFromElement, headingForPointFromElement,
headingIsHorizontal, headingIsHorizontal,
vectorToHeading, vectorToHeading,
@ -1025,7 +1031,14 @@ export const avoidRectangularCorner = (
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) { if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
// Top left // Top left
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) { const heading = headingForPoint(
nonRotatedPoint,
pointFrom(element.x, element.y),
);
if (
compareHeading(heading, HEADING_DOWN) ||
compareHeading(heading, HEADING_LEFT)
) {
return pointRotateRads<GlobalPoint>( return pointRotateRads<GlobalPoint>(
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y), pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
center, center,
@ -1042,7 +1055,14 @@ export const avoidRectangularCorner = (
nonRotatedPoint[1] > element.y + element.height nonRotatedPoint[1] > element.y + element.height
) { ) {
// Bottom left // Bottom left
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) { const heading = headingForPoint(
nonRotatedPoint,
pointFrom(element.x, element.y + element.height),
);
if (
compareHeading(heading, HEADING_DOWN) ||
compareHeading(heading, HEADING_RIGHT)
) {
return pointRotateRads( return pointRotateRads(
pointFrom( pointFrom(
element.x, element.x,
@ -1062,9 +1082,13 @@ export const avoidRectangularCorner = (
nonRotatedPoint[1] > element.y + element.height nonRotatedPoint[1] > element.y + element.height
) { ) {
// Bottom right // Bottom right
const heading = headingForPoint(
nonRotatedPoint,
pointFrom(element.x + element.width, element.y + element.height),
);
if ( if (
nonRotatedPoint[0] - element.x < compareHeading(heading, HEADING_DOWN) ||
element.width + FIXED_BINDING_DISTANCE compareHeading(heading, HEADING_LEFT)
) { ) {
return pointRotateRads( return pointRotateRads(
pointFrom( pointFrom(
@ -1088,9 +1112,13 @@ export const avoidRectangularCorner = (
nonRotatedPoint[1] < element.y nonRotatedPoint[1] < element.y
) { ) {
// Top right // Top right
const heading = headingForPoint(
nonRotatedPoint,
pointFrom(element.x + element.width, element.y),
);
if ( if (
nonRotatedPoint[0] - element.x < compareHeading(heading, HEADING_UP) ||
element.width + FIXED_BINDING_DISTANCE compareHeading(heading, HEADING_LEFT)
) { ) {
return pointRotateRads( return pointRotateRads(
pointFrom( pointFrom(
@ -1108,6 +1136,17 @@ export const avoidRectangularCorner = (
); );
} }
// Break up explicit border bindings to have better elbow arrow routing
if (p[0] === element.x) {
return pointFrom(p[0] - FIXED_BINDING_DISTANCE, p[1]);
} else if (p[0] === element.x + element.width) {
return pointFrom(p[0] + FIXED_BINDING_DISTANCE, p[1]);
} else if (p[1] === element.y) {
return pointFrom(p[0], p[1] - FIXED_BINDING_DISTANCE);
} else if (p[1] === element.y + element.height) {
return pointFrom(p[0], p[1] + FIXED_BINDING_DISTANCE);
}
return p; return p;
}; };

View File

@ -52,7 +52,7 @@ import {
type NonDeletedSceneElementsMap, type NonDeletedSceneElementsMap,
} from "./types"; } from "./types";
import { aabbForElement, pointInsideBounds } from "./shapes"; import { aabbForElement, aabbForPoints, pointInsideBounds } from "./shapes";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
import type { Heading } from "./heading"; import type { Heading } from "./heading";
@ -65,6 +65,8 @@ import type {
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./types"; } from "./types";
import { debugDrawBounds } from "@excalidraw/utils/visualdebug";
type GridAddress = [number, number] & { _brand: "gridaddress" }; type GridAddress = [number, number] & { _brand: "gridaddress" };
type Node = { type Node = {
@ -106,8 +108,32 @@ type ElbowArrowData = {
hoveredEndElement: ExcalidrawBindableElement | null; hoveredEndElement: ExcalidrawBindableElement | null;
}; };
const DEDUP_TRESHOLD = 1; const calculateDedupTreshhold = <Point extends GlobalPoint | LocalPoint>(
export const BASE_PADDING = 40; a: Point,
b: Point,
) => 1 + pointDistance(a, b) / 300;
const calculatePadding = (
aabb: Bounds,
startBoundingBox: Bounds,
endBoundingBox: Bounds,
) => {
return Math.max(
Math.min(
Math.hypot(
startBoundingBox[2] - startBoundingBox[0],
startBoundingBox[3] - startBoundingBox[1],
) / 4,
Math.hypot(
endBoundingBox[2] - endBoundingBox[0],
endBoundingBox[3] - endBoundingBox[1],
) / 4,
Math.hypot(aabb[2] - aabb[0], aabb[3] - aabb[1]) / 4,
40,
),
30,
);
};
const handleSegmentRenormalization = ( const handleSegmentRenormalization = (
arrow: ExcalidrawElbowArrowElement, arrow: ExcalidrawElbowArrowElement,
@ -183,7 +209,11 @@ const handleSegmentRenormalization = (
if ( if (
// Remove segments that are too short // 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 = const prevPrevSegmentIdx =
nextFixedSegments?.findIndex((segment) => segment.index === i - 2) ?? nextFixedSegments?.findIndex((segment) => segment.index === i - 2) ??
@ -359,6 +389,10 @@ const handleSegmentRelease = (
null, null,
); );
if (!restoredPoints) {
return {};
}
const nextPoints: GlobalPoint[] = []; const nextPoints: GlobalPoint[] = [];
// First part of the arrow are the old points // First part of the arrow are the old points
@ -463,6 +497,13 @@ const handleSegmentMove = (
hoveredStartElement: ExcalidrawBindableElement | null, hoveredStartElement: ExcalidrawBindableElement | null,
hoveredEndElement: ExcalidrawBindableElement | null, hoveredEndElement: ExcalidrawBindableElement | null,
): ElementUpdate<ExcalidrawElbowArrowElement> => { ): ElementUpdate<ExcalidrawElbowArrowElement> => {
const BASE_PADDING = calculatePadding(
aabbForElement(arrow),
hoveredStartElement
? aabbForElement(hoveredStartElement)
: [10, 10, 10, 10],
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
);
const activelyModifiedSegmentIdx = fixedSegments const activelyModifiedSegmentIdx = fixedSegments
.map((segment, i) => { .map((segment, i) => {
if ( if (
@ -707,6 +748,13 @@ const handleEndpointDrag = (
hoveredStartElement: ExcalidrawBindableElement | null, hoveredStartElement: ExcalidrawBindableElement | null,
hoveredEndElement: ExcalidrawBindableElement | null, hoveredEndElement: ExcalidrawBindableElement | null,
) => { ) => {
const BASE_PADDING = calculatePadding(
aabbForPoints([startGlobalPoint, endGlobalPoint]),
hoveredStartElement
? aabbForElement(hoveredStartElement)
: [10, 10, 10, 10],
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
);
let startIsSpecial = arrow.startIsSpecial ?? null; let startIsSpecial = arrow.startIsSpecial ?? null;
let endIsSpecial = arrow.endIsSpecial ?? null; let endIsSpecial = arrow.endIsSpecial ?? null;
const globalUpdatedPoints = updatedPoints.map((p, i) => const globalUpdatedPoints = updatedPoints.map((p, i) =>
@ -741,6 +789,7 @@ const handleEndpointDrag = (
// Calculate the moving second point connection and add the start point // Calculate the moving second point connection and add the start point
{ {
startIsSpecial = arrow.startIsSpecial && globalUpdatedPoints.length > 2;
const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1]; const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1];
const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2]; const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2];
const startIsHorizontal = headingIsHorizontal(startHeading); const startIsHorizontal = headingIsHorizontal(startHeading);
@ -801,6 +850,7 @@ const handleEndpointDrag = (
// Calculate the moving second to last point connection // Calculate the moving second to last point connection
{ {
endIsSpecial = arrow.endIsSpecial && globalUpdatedPoints.length > 2;
const secondToLastPoint = const secondToLastPoint =
globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)]; globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)];
const thirdToLastPoint = const thirdToLastPoint =
@ -1293,29 +1343,28 @@ const getElbowArrowData = (
endGlobalPoint[0] + 2, endGlobalPoint[0] + 2,
endGlobalPoint[1] + 2, endGlobalPoint[1] + 2,
] as Bounds; ] as Bounds;
const startElementBounds = hoveredStartElement const BASE_PADDING = calculatePadding(
? aabbForElement( aabbForPoints([startGlobalPoint, endGlobalPoint]),
hoveredStartElement, hoveredStartElement
offsetFromHeading( ? aabbForElement(hoveredStartElement)
: [10, 10, 10, 10],
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
);
const startOffsets = offsetFromHeading(
startHeading, startHeading,
arrow.startArrowhead arrow.startArrowhead ? FIXED_BINDING_DISTANCE * 4 : FIXED_BINDING_DISTANCE,
? FIXED_BINDING_DISTANCE * 6
: FIXED_BINDING_DISTANCE * 2,
1, 1,
), );
) const endOffsets = offsetFromHeading(
endHeading,
arrow.endArrowhead ? FIXED_BINDING_DISTANCE * 4 : FIXED_BINDING_DISTANCE,
1,
);
const startElementBounds = hoveredStartElement
? aabbForElement(hoveredStartElement, startOffsets)
: startPointBounds; : startPointBounds;
const endElementBounds = hoveredEndElement const endElementBounds = hoveredEndElement
? aabbForElement( ? aabbForElement(hoveredEndElement, endOffsets)
hoveredEndElement,
offsetFromHeading(
endHeading,
arrow.endArrowhead
? FIXED_BINDING_DISTANCE * 6
: FIXED_BINDING_DISTANCE * 2,
1,
),
)
: endPointBounds; : endPointBounds;
const boundsOverlap = const boundsOverlap =
pointInsideBounds( pointInsideBounds(
@ -1358,7 +1407,7 @@ const getElbowArrowData = (
: BASE_PADDING - : BASE_PADDING -
(arrow.startArrowhead (arrow.startArrowhead
? FIXED_BINDING_DISTANCE * 6 ? FIXED_BINDING_DISTANCE * 6
: FIXED_BINDING_DISTANCE * 2), : FIXED_BINDING_DISTANCE),
BASE_PADDING, BASE_PADDING,
), ),
boundsOverlap boundsOverlap
@ -1374,13 +1423,29 @@ const getElbowArrowData = (
: BASE_PADDING - : BASE_PADDING -
(arrow.endArrowhead (arrow.endArrowhead
? FIXED_BINDING_DISTANCE * 6 ? FIXED_BINDING_DISTANCE * 6
: FIXED_BINDING_DISTANCE * 2), : FIXED_BINDING_DISTANCE),
BASE_PADDING, BASE_PADDING,
), ),
boundsOverlap, boundsOverlap,
hoveredStartElement && aabbForElement(hoveredStartElement), hoveredStartElement
hoveredEndElement && aabbForElement(hoveredEndElement), ? aabbForElement(hoveredStartElement)
: startPointBounds,
hoveredEndElement ? aabbForElement(hoveredEndElement) : endPointBounds,
); );
debugDrawBounds(startElementBounds, {
permanent: false,
color: "red",
});
debugDrawBounds(endElementBounds, {
permanent: false,
color: "green",
});
debugDrawBounds(dynamicAABBs, {
permanent: false,
color: "blue",
});
const startDonglePosition = getDonglePosition( const startDonglePosition = getDonglePosition(
dynamicAABBs[0], dynamicAABBs[0],
startHeading, startHeading,
@ -1651,11 +1716,11 @@ const generateDynamicAABBs = (
a: Bounds, a: Bounds,
b: Bounds, b: Bounds,
common: Bounds, common: Bounds,
startDifference?: [number, number, number, number], startDifference: [number, number, number, number],
endDifference?: [number, number, number, number], endDifference: [number, number, number, number],
disableSideHack?: boolean, disableSideHack: boolean,
startElementBounds?: Bounds | null, startElementBounds: Bounds,
endElementBounds?: Bounds | null, endElementBounds: Bounds,
): Bounds[] => { ): Bounds[] => {
const startEl = startElementBounds ?? a; const startEl = startElementBounds ?? a;
const endEl = endElementBounds ?? b; const endEl = endElementBounds ?? b;
@ -1735,15 +1800,24 @@ const generateDynamicAABBs = (
(second[0] + second[2]) / 2, (second[0] + second[2]) / 2,
(second[1] + second[3]) / 2, (second[1] + second[3]) / 2,
]; ];
if (b[0] > a[2] && a[1] > b[3]) { if (
endElementBounds[0] > startElementBounds[2] &&
startElementBounds[1] > endElementBounds[3]
) {
// BOTTOM LEFT // BOTTOM LEFT
const cX = first[2] + (second[0] - first[2]) / 2; const cX = first[2] + (second[0] - first[2]) / 2;
const cY = second[3] + (first[1] - second[3]) / 2; const cY = second[3] + (first[1] - second[3]) / 2;
if ( if (
vectorCross( vectorCross(
vector(a[2] - endCenterX, a[1] - endCenterY), vector(
vector(a[0] - endCenterX, a[3] - endCenterY), startElementBounds[2] - endCenterX,
startElementBounds[1] - endCenterY,
),
vector(
startElementBounds[0] - endCenterX,
startElementBounds[3] - endCenterY,
),
) > 0 ) > 0
) { ) {
return [ return [
@ -1756,15 +1830,24 @@ const generateDynamicAABBs = (
[first[0], cY, first[2], first[3]], [first[0], cY, first[2], first[3]],
[second[0], second[1], second[2], cY], [second[0], second[1], second[2], cY],
]; ];
} else if (a[2] < b[0] && a[3] < b[1]) { } else if (
startElementBounds[2] < endElementBounds[0] &&
startElementBounds[3] < endElementBounds[1]
) {
// TOP LEFT // TOP LEFT
const cX = first[2] + (second[0] - first[2]) / 2; const cX = first[2] + (second[0] - first[2]) / 2;
const cY = first[3] + (second[1] - first[3]) / 2; const cY = first[3] + (second[1] - first[3]) / 2;
if ( if (
vectorCross( vectorCross(
vector(a[0] - endCenterX, a[1] - endCenterY), vector(
vector(a[2] - endCenterX, a[3] - endCenterY), startElementBounds[0] - endCenterX,
startElementBounds[1] - endCenterY,
),
vector(
startElementBounds[2] - endCenterX,
startElementBounds[3] - endCenterY,
),
) > 0 ) > 0
) { ) {
return [ return [
@ -1777,15 +1860,24 @@ const generateDynamicAABBs = (
[first[0], first[1], cX, first[3]], [first[0], first[1], cX, first[3]],
[cX, second[1], second[2], second[3]], [cX, second[1], second[2], second[3]],
]; ];
} else if (a[0] > b[2] && a[3] < b[1]) { } else if (
startElementBounds[0] > endElementBounds[2] &&
startElementBounds[3] < endElementBounds[1]
) {
// TOP RIGHT // TOP RIGHT
const cX = second[2] + (first[0] - second[2]) / 2; const cX = second[2] + (first[0] - second[2]) / 2;
const cY = first[3] + (second[1] - first[3]) / 2; const cY = first[3] + (second[1] - first[3]) / 2;
if ( if (
vectorCross( vectorCross(
vector(a[2] - endCenterX, a[1] - endCenterY), vector(
vector(a[0] - endCenterX, a[3] - endCenterY), startElementBounds[2] - endCenterX,
startElementBounds[1] - endCenterY,
),
vector(
startElementBounds[0] - endCenterX,
startElementBounds[3] - endCenterY,
),
) > 0 ) > 0
) { ) {
return [ return [
@ -1798,15 +1890,24 @@ const generateDynamicAABBs = (
[first[0], first[1], first[2], cY], [first[0], first[1], first[2], cY],
[second[0], cY, second[2], second[3]], [second[0], cY, second[2], second[3]],
]; ];
} else if (a[0] > b[2] && a[1] > b[3]) { } else if (
startElementBounds[0] > endElementBounds[2] &&
startElementBounds[1] > endElementBounds[3]
) {
// BOTTOM RIGHT // BOTTOM RIGHT
const cX = second[2] + (first[0] - second[2]) / 2; const cX = second[2] + (first[0] - second[2]) / 2;
const cY = second[3] + (first[1] - second[3]) / 2; const cY = second[3] + (first[1] - second[3]) / 2;
if ( if (
vectorCross( vectorCross(
vector(a[0] - endCenterX, a[1] - endCenterY), vector(
vector(a[2] - endCenterX, a[3] - endCenterY), startElementBounds[0] - endCenterX,
startElementBounds[1] - endCenterY,
),
vector(
startElementBounds[2] - endCenterX,
startElementBounds[3] - endCenterY,
),
) > 0 ) > 0
) { ) {
return [ return [
@ -2088,16 +2189,11 @@ const normalizeArrowElementUpdate = (
nextFixedSegments: readonly FixedSegment[] | null, nextFixedSegments: readonly FixedSegment[] | null,
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"], startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"], endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
): { ): ElementUpdate<ExcalidrawElbowArrowElement> => {
points: LocalPoint[]; if (global.length === 0) {
x: number; return {};
y: number; }
width: number;
height: number;
fixedSegments: readonly FixedSegment[] | null;
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
} => {
const offsetX = global[0][0]; const offsetX = global[0][0];
const offsetY = global[0][1]; const offsetY = global[0][1];
let points = global.map((p) => let points = global.map((p) =>
@ -2185,7 +2281,10 @@ const removeElbowArrowShortSegments = (
const prev = points[idx - 1]; const prev = points[idx - 1];
const prevDist = pointDistance(prev, p); const prevDist = pointDistance(prev, p);
return prevDist > DEDUP_TRESHOLD; return (
prevDist >
calculateDedupTreshhold(points[idx - 2] ?? prev, points[idx + 1] ?? p)
);
}); });
} }
@ -2288,13 +2387,16 @@ const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
export const validateElbowPoints = <P extends GlobalPoint | LocalPoint>( export const validateElbowPoints = <P extends GlobalPoint | LocalPoint>(
points: readonly P[], points: readonly P[],
tolerance: number = DEDUP_TRESHOLD, tolerance?: number,
) => ) =>
points points
.slice(1) .slice(1)
.map( .map((p, i) => {
(p, i) => const t =
Math.abs(p[0] - points[i][0]) < tolerance || tolerance ??
Math.abs(p[1] - points[i][1]) < 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); .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 * Get the axis-aligned bounding box for a given element
*/ */

View File

@ -195,7 +195,7 @@ describe("elbow arrow routing", () => {
expect(arrow.startBinding).not.toBe(null); expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null);
h.app.scene.mutateElement(arrow, { scene.mutateElement(arrow, {
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)], points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
}); });
@ -295,11 +295,11 @@ describe("elbow arrow ui", () => {
) as HTMLInputElement; ) as HTMLInputElement;
UI.updateInput(inputAngle, String("40")); UI.updateInput(inputAngle, String("40"));
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[35, 0], [34.7791, 0],
[35, 165], [34.7791, 164.67],
[103, 165], [102.931, 164.67],
]); ]);
}); });

View File

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

View File

@ -73,12 +73,12 @@ describe("flipping re-centers selection", () => {
API.executeAction(actionFlipHorizontal); API.executeAction(actionFlipHorizontal);
const rec1 = h.elements.find((el) => el.id === "rec1")!; const rec1 = h.elements.find((el) => el.id === "rec1")!;
expect(rec1.x).toBeCloseTo(100, 0); expect(rec1.x).toBeCloseTo(97.8678, 0);
expect(rec1.y).toBeCloseTo(100, 0); expect(rec1.y).toBeCloseTo(97.444, 0);
const rec2 = h.elements.find((el) => el.id === "rec2")!; const rec2 = h.elements.find((el) => el.id === "rec2")!;
expect(rec2.x).toBeCloseTo(220, 0); expect(rec2.x).toBeCloseTo(218, 0);
expect(rec2.y).toBeCloseTo(250, 0); expect(rec2.y).toBeCloseTo(247, 0);
}); });
}); });