|
|
|
|
@ -1,11 +1,15 @@
@@ -1,11 +1,15 @@
|
|
|
|
|
import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common"; |
|
|
|
|
|
|
|
|
|
import { |
|
|
|
|
normalizeRadians, |
|
|
|
|
pointFrom, |
|
|
|
|
pointFromVector, |
|
|
|
|
pointRotateRads, |
|
|
|
|
pointScaleFromOrigin, |
|
|
|
|
radiansToDegrees, |
|
|
|
|
pointsEqual, |
|
|
|
|
triangleIncludesPoint, |
|
|
|
|
vectorCross, |
|
|
|
|
vectorFromPoint, |
|
|
|
|
vectorScale, |
|
|
|
|
} from "@excalidraw/math"; |
|
|
|
|
|
|
|
|
|
import type { |
|
|
|
|
@ -13,7 +17,6 @@ import type {
@@ -13,7 +17,6 @@ import type {
|
|
|
|
|
GlobalPoint, |
|
|
|
|
Triangle, |
|
|
|
|
Vector, |
|
|
|
|
Radians, |
|
|
|
|
} from "@excalidraw/math"; |
|
|
|
|
|
|
|
|
|
import { getCenterForBounds, type Bounds } from "./bounds"; |
|
|
|
|
@ -26,24 +29,6 @@ export const HEADING_LEFT = [-1, 0] as Heading;
@@ -26,24 +29,6 @@ export const HEADING_LEFT = [-1, 0] as Heading;
|
|
|
|
|
export const HEADING_UP = [0, -1] as Heading; |
|
|
|
|
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 => { |
|
|
|
|
const [x, y] = vec; |
|
|
|
|
const absX = Math.abs(x); |
|
|
|
|
@ -76,87 +61,179 @@ export const headingIsHorizontal = (a: Heading) =>
@@ -76,87 +61,179 @@ export const headingIsHorizontal = (a: Heading) =>
|
|
|
|
|
|
|
|
|
|
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a); |
|
|
|
|
|
|
|
|
|
// 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>( |
|
|
|
|
const headingForPointFromDiamondElement = ( |
|
|
|
|
element: Readonly<ExcalidrawBindableElement>, |
|
|
|
|
aabb: Readonly<Bounds>, |
|
|
|
|
p: Readonly<Point>, |
|
|
|
|
point: Readonly<GlobalPoint>, |
|
|
|
|
): Heading => { |
|
|
|
|
const SEARCH_CONE_MULTIPLIER = 2; |
|
|
|
|
|
|
|
|
|
const midPoint = getCenterForBounds(aabb); |
|
|
|
|
|
|
|
|
|
if (element.type === "diamond") { |
|
|
|
|
if (p[0] < element.x) { |
|
|
|
|
return HEADING_LEFT; |
|
|
|
|
} else if (p[1] < element.y) { |
|
|
|
|
return HEADING_UP; |
|
|
|
|
} else if (p[0] > element.x + element.width) { |
|
|
|
|
return HEADING_RIGHT; |
|
|
|
|
} else if (p[1] > element.y + element.height) { |
|
|
|
|
return HEADING_DOWN; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const top = pointRotateRads( |
|
|
|
|
pointScaleFromOrigin( |
|
|
|
|
pointFrom(element.x + element.width / 2, element.y), |
|
|
|
|
if (isDevEnv() || isTestEnv()) { |
|
|
|
|
invariant( |
|
|
|
|
element.width > 0 && element.height > 0, |
|
|
|
|
"Diamond element has no width or height", |
|
|
|
|
); |
|
|
|
|
invariant( |
|
|
|
|
!pointsEqual(midPoint, point), |
|
|
|
|
"The point is too close to the element mid point to determine heading", |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const SHRINK = 0.95; // Rounded elements tolerance
|
|
|
|
|
const top = pointFromVector( |
|
|
|
|
vectorScale( |
|
|
|
|
vectorFromPoint( |
|
|
|
|
pointRotateRads( |
|
|
|
|
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
SEARCH_CONE_MULTIPLIER, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
); |
|
|
|
|
const right = pointRotateRads( |
|
|
|
|
pointScaleFromOrigin( |
|
|
|
|
pointFrom(element.x + element.width, element.y + element.height / 2), |
|
|
|
|
SHRINK, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
); |
|
|
|
|
const right = pointFromVector( |
|
|
|
|
vectorScale( |
|
|
|
|
vectorFromPoint( |
|
|
|
|
pointRotateRads( |
|
|
|
|
pointFrom<GlobalPoint>( |
|
|
|
|
element.x + element.width, |
|
|
|
|
element.y + element.height / 2, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
SEARCH_CONE_MULTIPLIER, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
); |
|
|
|
|
const bottom = pointRotateRads( |
|
|
|
|
pointScaleFromOrigin( |
|
|
|
|
pointFrom(element.x + element.width / 2, element.y + element.height), |
|
|
|
|
SHRINK, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
); |
|
|
|
|
const bottom = pointFromVector( |
|
|
|
|
vectorScale( |
|
|
|
|
vectorFromPoint( |
|
|
|
|
pointRotateRads( |
|
|
|
|
pointFrom<GlobalPoint>( |
|
|
|
|
element.x + element.width / 2, |
|
|
|
|
element.y + element.height, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
SEARCH_CONE_MULTIPLIER, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
); |
|
|
|
|
const left = pointRotateRads( |
|
|
|
|
pointScaleFromOrigin( |
|
|
|
|
pointFrom(element.x, element.y + element.height / 2), |
|
|
|
|
SHRINK, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
); |
|
|
|
|
const left = pointFromVector( |
|
|
|
|
vectorScale( |
|
|
|
|
vectorFromPoint( |
|
|
|
|
pointRotateRads( |
|
|
|
|
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
SEARCH_CONE_MULTIPLIER, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
element.angle, |
|
|
|
|
); |
|
|
|
|
SHRINK, |
|
|
|
|
), |
|
|
|
|
midPoint, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
triangleIncludesPoint<Point>([top, right, midPoint] as Triangle<Point>, p) |
|
|
|
|
) { |
|
|
|
|
return headingForDiamond(top, right); |
|
|
|
|
} else if ( |
|
|
|
|
triangleIncludesPoint<Point>( |
|
|
|
|
[right, bottom, midPoint] as Triangle<Point>, |
|
|
|
|
p, |
|
|
|
|
) |
|
|
|
|
) { |
|
|
|
|
return headingForDiamond(right, bottom); |
|
|
|
|
} else if ( |
|
|
|
|
triangleIncludesPoint<Point>( |
|
|
|
|
[bottom, left, midPoint] as Triangle<Point>, |
|
|
|
|
p, |
|
|
|
|
) |
|
|
|
|
) { |
|
|
|
|
return headingForDiamond(bottom, left); |
|
|
|
|
} |
|
|
|
|
// Corners
|
|
|
|
|
if ( |
|
|
|
|
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, right)) <= |
|
|
|
|
0 && |
|
|
|
|
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, left)) > 0 |
|
|
|
|
) { |
|
|
|
|
return headingForPoint(top, midPoint); |
|
|
|
|
} else if ( |
|
|
|
|
vectorCross( |
|
|
|
|
vectorFromPoint(point, right), |
|
|
|
|
vectorFromPoint(right, bottom), |
|
|
|
|
) <= 0 && |
|
|
|
|
vectorCross(vectorFromPoint(point, right), vectorFromPoint(right, top)) > 0 |
|
|
|
|
) { |
|
|
|
|
return headingForPoint(right, midPoint); |
|
|
|
|
} else if ( |
|
|
|
|
vectorCross( |
|
|
|
|
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( |
|
|
|
|
|