aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/element/heading.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/element/heading.ts')
-rw-r--r--packages/excalidraw/element/heading.ts202
1 files changed, 202 insertions, 0 deletions
diff --git a/packages/excalidraw/element/heading.ts b/packages/excalidraw/element/heading.ts
new file mode 100644
index 0000000..94e8633
--- /dev/null
+++ b/packages/excalidraw/element/heading.ts
@@ -0,0 +1,202 @@
+import type {
+ LocalPoint,
+ GlobalPoint,
+ Triangle,
+ Vector,
+ Radians,
+} from "@excalidraw/math";
+import {
+ pointFrom,
+ pointRotateRads,
+ pointScaleFromOrigin,
+ radiansToDegrees,
+ triangleIncludesPoint,
+ vectorFromPoint,
+} from "@excalidraw/math";
+import { getCenterForBounds, type Bounds } from "./bounds";
+import type { ExcalidrawBindableElement } from "./types";
+
+export const HEADING_RIGHT = [1, 0] as Heading;
+export const HEADING_DOWN = [0, 1] as Heading;
+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(
+ 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);
+ const absY = Math.abs(y);
+ if (x > absY) {
+ return HEADING_RIGHT;
+ } else if (x <= -absY) {
+ return HEADING_LEFT;
+ } else if (y > absX) {
+ return HEADING_DOWN;
+ }
+ return HEADING_UP;
+};
+
+export const headingForPoint = <P extends GlobalPoint | LocalPoint>(
+ p: P,
+ o: P,
+) => vectorToHeading(vectorFromPoint<P>(p, o));
+
+export const headingForPointIsHorizontal = <P extends GlobalPoint | LocalPoint>(
+ p: P,
+ o: P,
+) => headingIsHorizontal(headingForPoint<P>(p, o));
+
+export const compareHeading = (a: Heading, b: Heading) =>
+ a[0] === b[0] && a[1] === b[1];
+
+export const headingIsHorizontal = (a: Heading) =>
+ compareHeading(a, HEADING_RIGHT) || compareHeading(a, HEADING_LEFT);
+
+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 | LocalPoint,
+>(
+ element: Readonly<ExcalidrawBindableElement>,
+ aabb: Readonly<Bounds>,
+ p: Readonly<Point>,
+): 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),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ),
+ midPoint,
+ element.angle,
+ );
+ const right = pointRotateRads(
+ pointScaleFromOrigin(
+ pointFrom(element.x + element.width, element.y + element.height / 2),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ),
+ midPoint,
+ element.angle,
+ );
+ const bottom = pointRotateRads(
+ pointScaleFromOrigin(
+ pointFrom(element.x + element.width / 2, element.y + element.height),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ),
+ midPoint,
+ element.angle,
+ );
+ const left = pointRotateRads(
+ pointScaleFromOrigin(
+ pointFrom(element.x, element.y + element.height / 2),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ),
+ midPoint,
+ element.angle,
+ );
+
+ 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);
+ }
+
+ return headingForDiamond(left, top);
+ }
+
+ const topLeft = pointScaleFromOrigin(
+ pointFrom(aabb[0], aabb[1]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+ const topRight = pointScaleFromOrigin(
+ pointFrom(aabb[2], aabb[1]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+ const bottomLeft = pointScaleFromOrigin(
+ pointFrom(aabb[0], aabb[3]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+ const bottomRight = pointScaleFromOrigin(
+ pointFrom(aabb[2], aabb[3]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+
+ return triangleIncludesPoint<Point>(
+ [topLeft, topRight, midPoint] as Triangle<Point>,
+ p,
+ )
+ ? HEADING_UP
+ : triangleIncludesPoint<Point>(
+ [topRight, bottomRight, midPoint] as Triangle<Point>,
+ p,
+ )
+ ? HEADING_RIGHT
+ : triangleIncludesPoint<Point>(
+ [bottomRight, bottomLeft, midPoint] as Triangle<Point>,
+ p,
+ )
+ ? HEADING_DOWN
+ : HEADING_LEFT;
+};
+
+export const flipHeading = (h: Heading): Heading =>
+ [
+ h[0] === 0 ? 0 : h[0] > 0 ? -1 : 1,
+ h[1] === 0 ? 0 : h[1] > 0 ? -1 : 1,
+ ] as Heading;