diff --git a/packages/math/src/vector.ts b/packages/math/src/vector.ts index 12682fcd9..c520fce24 100644 --- a/packages/math/src/vector.ts +++ b/packages/math/src/vector.ts @@ -21,13 +21,23 @@ export function vector( * * @param p The point to turn into a vector * @param origin The origin point in a given coordiante system - * @returns The created vector from the point and the origin + * @param threshold The threshold to consider the vector as 'undefined' + * @param defaultValue The default value to return if the vector is 'undefined' + * @returns The created vector from the point and the origin or default */ export function vectorFromPoint( p: Point, origin: Point = [0, 0] as Point, + threshold?: number, + defaultValue: Vector = [0, 1] as Vector, ): Vector { - return vector(p[0] - origin[0], p[1] - origin[1]); + const vec = vector(p[0] - origin[0], p[1] - origin[1]); + + if (threshold && vectorMagnitudeSq(vec) < threshold * threshold) { + return defaultValue; + } + + return vec; } /** diff --git a/packages/utils/src/collision.ts b/packages/utils/src/collision.ts index ced2b54a0..55a60b74c 100644 --- a/packages/utils/src/collision.ts +++ b/packages/utils/src/collision.ts @@ -1,13 +1,11 @@ import { lineSegment, pointFrom, - polygonIncludesPoint, - pointOnLineSegment, type GlobalPoint, - type LocalPoint, - type Polygon, - vectorCross, vectorFromPoint, + vectorNormalize, + vectorScale, + pointFromVector, } from "@excalidraw/math"; import { intersectElementWithLineSegment } from "@excalidraw/element/collision"; @@ -16,16 +14,17 @@ import { elementCenterPoint } from "@excalidraw/common"; import { distanceToElement } from "@excalidraw/element/distance"; -import { isLinearElement } from "@excalidraw/excalidraw"; +import { getCommonBounds, isLinearElement } from "@excalidraw/excalidraw"; import { isFreeDrawElement } from "@excalidraw/element/typeChecks"; import { isPathALoop } from "@excalidraw/element/shapes"; +import { + debugDrawLine, + debugDrawPoint, +} from "@excalidraw/excalidraw/visualdebug"; + import type { ExcalidrawElement } from "@excalidraw/element/types"; -import type { Curve } from "@excalidraw/math"; - -import type { Polyline } from "./shape"; - // check if the given point is considered on the given shape's border export const isPointOnShape = ( point: GlobalPoint, @@ -44,15 +43,33 @@ export const isPointInShape = ( ) => { if (isLinearElement(element) || isFreeDrawElement(element)) { if (isPathALoop(element.points)) { - // for a closed path, we need to check if the point is inside the path - const r = isPointInClosedPath( - element.points.map((p) => - pointFrom(element.x + p[0], element.y + p[1]), - ), - point, + const [minX, minY, maxX, maxY] = getCommonBounds([element]); + const center = pointFrom( + (maxX + minX) / 2, + (maxY + minY) / 2, ); - //console.log(r); - return r; + const otherPoint = pointFromVector( + vectorScale( + vectorNormalize(vectorFromPoint(point, center, 0.1)), + Math.max(element.width, element.height) * 2, + ), + center, + ); + const intersector = lineSegment(point, otherPoint); + + // What about being on the center exactly? + const intersections = intersectElementWithLineSegment( + element, + intersector, + ); + + const hit = intersections.length % 2 === 1; + + debugDrawLine(intersector, { color: hit ? "green" : "red" }); + debugDrawPoint(point, { color: "black" }); + debugDrawPoint(otherPoint, { color: "blue" }); + + return hit; } // There isn't any "inside" for a non-looping path @@ -66,91 +83,3 @@ export const isPointInShape = ( return intersections.length === 0; }; - -/** - * Determine if a closed path contains a point. - * - * Implementation notes: We'll use the fact that the path is a consecutive - * sequence of line segments, these line segments have a winding order and - * the fact that if a point is inside the closed path, the cross product of the - * start point of a line segment to the point p and the end point of the line - * segment will be negative for all segments. - * - * @param points - * @param p - */ -const isPointInClosedPath = ( - points: readonly GlobalPoint[], - p: GlobalPoint, -) => { - const segments = points.slice(1).map((point, i) => { - return lineSegment(points[i], point); - }); - - return segments.every((segment) => { - const c = vectorCross( - vectorFromPoint(segment[0], p), - vectorFromPoint(segment[0], segment[1]), - ); - - return c < 0; - }); -}; - -// check if the given element is in the given bounds -export const isPointInBounds = ( - point: Point, - bounds: Polygon, -) => { - return polygonIncludesPoint(point, bounds); -}; - -const cubicBezierEquation = ( - curve: Curve, -) => { - const [p0, p1, p2, p3] = curve; - // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3 - return (t: number, idx: number) => - Math.pow(1 - t, 3) * p3[idx] + - 3 * t * Math.pow(1 - t, 2) * p2[idx] + - 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + - p0[idx] * Math.pow(t, 3); -}; - -const polyLineFromCurve = ( - curve: Curve, - segments = 10, -): Polyline => { - const equation = cubicBezierEquation(curve); - let startingPoint = [equation(0, 0), equation(0, 1)] as Point; - const lineSegments: Polyline = []; - let t = 0; - const increment = 1 / segments; - - for (let i = 0; i < segments; i++) { - t += increment; - if (t <= 1) { - const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1)); - lineSegments.push(lineSegment(startingPoint, nextPoint)); - startingPoint = nextPoint; - } - } - - return lineSegments; -}; - -export const pointOnCurve = ( - point: Point, - curve: Curve, - threshold: number, -) => { - return pointOnPolyline(point, polyLineFromCurve(curve), threshold); -}; - -export const pointOnPolyline = ( - point: Point, - polyline: Polyline, - threshold = 10e-5, -) => { - return polyline.some((line) => pointOnLineSegment(point, line, threshold)); -};