import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common"; import { pointFrom, pointFromVector, pointRotateRads, pointScaleFromOrigin, pointsEqual, triangleIncludesPoint, vectorCross, vectorFromPoint, vectorScale, } from "@excalidraw/math"; import type { LocalPoint, GlobalPoint, Triangle, Vector, } 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 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: P, o: P, ) => vectorToHeading(vectorFromPoint

(p, o)); export const headingForPointIsHorizontal =

( p: P, o: P, ) => headingIsHorizontal(headingForPoint

(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); const headingForPointFromDiamondElement = ( element: Readonly, aabb: Readonly, point: Readonly, ): Heading => { const midPoint = getCenterForBounds(aabb); 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(element.x + element.width / 2, element.y), midPoint, element.angle, ), midPoint, ), SHRINK, ), midPoint, ); const right = pointFromVector( vectorScale( vectorFromPoint( pointRotateRads( pointFrom( element.x + element.width, element.y + element.height / 2, ), midPoint, element.angle, ), midPoint, ), SHRINK, ), midPoint, ); const bottom = pointFromVector( vectorScale( vectorFromPoint( pointRotateRads( pointFrom( element.x + element.width / 2, element.y + element.height, ), midPoint, element.angle, ), midPoint, ), SHRINK, ), midPoint, ); const left = pointFromVector( vectorScale( vectorFromPoint( pointRotateRads( pointFrom(element.x, element.y + element.height / 2), midPoint, element.angle, ), midPoint, ), SHRINK, ), midPoint, ); // 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); }; // 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 = ( element: Readonly, aabb: Readonly, p: Readonly, ): Heading => { const SEARCH_CONE_MULTIPLIER = 2; const midPoint = getCenterForBounds(aabb); if (element.type === "diamond") { return headingForPointFromDiamondElement(element, aabb, p); } 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( [topLeft, topRight, midPoint] as Triangle, p, ) ? HEADING_UP : triangleIncludesPoint( [topRight, bottomRight, midPoint] as Triangle, p, ) ? HEADING_RIGHT : triangleIncludesPoint( [bottomRight, bottomLeft, midPoint] as Triangle, 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;