diff options
Diffstat (limited to 'packages/excalidraw/element/heading.ts')
| -rw-r--r-- | packages/excalidraw/element/heading.ts | 202 |
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; |
