Browse Source

fix: Rounded diamond edge elbow arrow U route (#9349)

pull/8887/merge
Márk Tolmács 8 months ago committed by GitHub
parent
commit
e48b63a0ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 251
      packages/element/src/heading.ts

251
packages/element/src/heading.ts

@ -1,11 +1,15 @@
import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
import { import {
normalizeRadians,
pointFrom, pointFrom,
pointFromVector,
pointRotateRads, pointRotateRads,
pointScaleFromOrigin, pointScaleFromOrigin,
radiansToDegrees, pointsEqual,
triangleIncludesPoint, triangleIncludesPoint,
vectorCross,
vectorFromPoint, vectorFromPoint,
vectorScale,
} from "@excalidraw/math"; } from "@excalidraw/math";
import type { import type {
@ -13,7 +17,6 @@ import type {
GlobalPoint, GlobalPoint,
Triangle, Triangle,
Vector, Vector,
Radians,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { getCenterForBounds, type Bounds } from "./bounds"; import { getCenterForBounds, type Bounds } from "./bounds";
@ -26,24 +29,6 @@ export const HEADING_LEFT = [-1, 0] as Heading;
export const HEADING_UP = [0, -1] as Heading; export const HEADING_UP = [0, -1] as Heading;
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1]; export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
a: Point,
b: Point,
) => {
const angle = radiansToDegrees(
normalizeRadians(Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians),
);
if (angle >= 315 || angle < 45) {
return HEADING_UP;
} else if (angle >= 45 && angle < 135) {
return HEADING_RIGHT;
} else if (angle >= 135 && angle < 225) {
return HEADING_DOWN;
}
return HEADING_LEFT;
};
export const vectorToHeading = (vec: Vector): Heading => { export const vectorToHeading = (vec: Vector): Heading => {
const [x, y] = vec; const [x, y] = vec;
const absX = Math.abs(x); const absX = Math.abs(x);
@ -76,87 +61,179 @@ export const headingIsHorizontal = (a: Heading) =>
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a); export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
// Gets the heading for the point by creating a bounding box around the rotated const headingForPointFromDiamondElement = (
// close fitting bounding box, then creating 4 search cones around the center of
// the external bbox.
export const headingForPointFromElement = <Point extends GlobalPoint>(
element: Readonly<ExcalidrawBindableElement>, element: Readonly<ExcalidrawBindableElement>,
aabb: Readonly<Bounds>, aabb: Readonly<Bounds>,
p: Readonly<Point>, point: Readonly<GlobalPoint>,
): Heading => { ): Heading => {
const SEARCH_CONE_MULTIPLIER = 2;
const midPoint = getCenterForBounds(aabb); const midPoint = getCenterForBounds(aabb);
if (element.type === "diamond") { if (isDevEnv() || isTestEnv()) {
if (p[0] < element.x) { invariant(
return HEADING_LEFT; element.width > 0 && element.height > 0,
} else if (p[1] < element.y) { "Diamond element has no width or height",
return HEADING_UP; );
} else if (p[0] > element.x + element.width) { invariant(
return HEADING_RIGHT; !pointsEqual(midPoint, point),
} else if (p[1] > element.y + element.height) { "The point is too close to the element mid point to determine heading",
return HEADING_DOWN; );
} }
const top = pointRotateRads( const SHRINK = 0.95; // Rounded elements tolerance
pointScaleFromOrigin( const top = pointFromVector(
pointFrom(element.x + element.width / 2, element.y), vectorScale(
vectorFromPoint(
pointRotateRads(
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
midPoint,
element.angle,
),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER,
), ),
midPoint, SHRINK,
element.angle, ),
); midPoint,
const right = pointRotateRads( );
pointScaleFromOrigin( const right = pointFromVector(
pointFrom(element.x + element.width, element.y + element.height / 2), vectorScale(
vectorFromPoint(
pointRotateRads(
pointFrom<GlobalPoint>(
element.x + element.width,
element.y + element.height / 2,
),
midPoint,
element.angle,
),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER,
), ),
midPoint, SHRINK,
element.angle, ),
); midPoint,
const bottom = pointRotateRads( );
pointScaleFromOrigin( const bottom = pointFromVector(
pointFrom(element.x + element.width / 2, element.y + element.height), vectorScale(
vectorFromPoint(
pointRotateRads(
pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height,
),
midPoint,
element.angle,
),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER,
), ),
midPoint, SHRINK,
element.angle, ),
); midPoint,
const left = pointRotateRads( );
pointScaleFromOrigin( const left = pointFromVector(
pointFrom(element.x, element.y + element.height / 2), vectorScale(
vectorFromPoint(
pointRotateRads(
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
midPoint,
element.angle,
),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER,
), ),
midPoint, SHRINK,
element.angle, ),
); midPoint,
);
if ( // Corners
triangleIncludesPoint<Point>([top, right, midPoint] as Triangle<Point>, p) if (
) { vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, right)) <=
return headingForDiamond(top, right); 0 &&
} else if ( vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, left)) > 0
triangleIncludesPoint<Point>( ) {
[right, bottom, midPoint] as Triangle<Point>, return headingForPoint(top, midPoint);
p, } else if (
) vectorCross(
) { vectorFromPoint(point, right),
return headingForDiamond(right, bottom); vectorFromPoint(right, bottom),
} else if ( ) <= 0 &&
triangleIncludesPoint<Point>( vectorCross(vectorFromPoint(point, right), vectorFromPoint(right, top)) > 0
[bottom, left, midPoint] as Triangle<Point>, ) {
p, return headingForPoint(right, midPoint);
) } else if (
) { vectorCross(
return headingForDiamond(bottom, left); vectorFromPoint(point, bottom),
} vectorFromPoint(bottom, left),
) <= 0 &&
vectorCross(
vectorFromPoint(point, bottom),
vectorFromPoint(bottom, right),
) > 0
) {
return headingForPoint(bottom, midPoint);
} else if (
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, top)) <=
0 &&
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, bottom)) > 0
) {
return headingForPoint(left, midPoint);
}
// Sides
if (
vectorCross(
vectorFromPoint(point, midPoint),
vectorFromPoint(top, midPoint),
) <= 0 &&
vectorCross(
vectorFromPoint(point, midPoint),
vectorFromPoint(right, midPoint),
) > 0
) {
const p = element.width > element.height ? top : right;
return headingForPoint(p, midPoint);
} else if (
vectorCross(
vectorFromPoint(point, midPoint),
vectorFromPoint(right, midPoint),
) <= 0 &&
vectorCross(
vectorFromPoint(point, midPoint),
vectorFromPoint(bottom, midPoint),
) > 0
) {
const p = element.width > element.height ? bottom : right;
return headingForPoint(p, midPoint);
} else if (
vectorCross(
vectorFromPoint(point, midPoint),
vectorFromPoint(bottom, midPoint),
) <= 0 &&
vectorCross(
vectorFromPoint(point, midPoint),
vectorFromPoint(left, midPoint),
) > 0
) {
const p = element.width > element.height ? bottom : left;
return headingForPoint(p, midPoint);
}
const p = element.width > element.height ? top : left;
return headingForPoint(p, midPoint);
};
return headingForDiamond(left, top); // Gets the heading for the point by creating a bounding box around the rotated
// close fitting bounding box, then creating 4 search cones around the center of
// the external bbox.
export const headingForPointFromElement = <Point extends GlobalPoint>(
element: Readonly<ExcalidrawBindableElement>,
aabb: Readonly<Bounds>,
p: Readonly<Point>,
): Heading => {
const SEARCH_CONE_MULTIPLIER = 2;
const midPoint = getCenterForBounds(aabb);
if (element.type === "diamond") {
return headingForPointFromDiamondElement(element, aabb, p);
} }
const topLeft = pointScaleFromOrigin( const topLeft = pointScaleFromOrigin(

Loading…
Cancel
Save