[skip ci] inverted polygon hit test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs 2025-05-09 08:43:02 +02:00
parent 3b037a7d82
commit e1b81480ac
No known key found for this signature in database
2 changed files with 47 additions and 108 deletions

View File

@ -21,13 +21,23 @@ export function vector(
* *
* @param p The point to turn into a vector * @param p The point to turn into a vector
* @param origin The origin point in a given coordiante system * @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<Point extends GlobalPoint | LocalPoint>( export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>(
p: Point, p: Point,
origin: Point = [0, 0] as Point, origin: Point = [0, 0] as Point,
threshold?: number,
defaultValue: Vector = [0, 1] as Vector,
): 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;
} }
/** /**

View File

@ -1,13 +1,11 @@
import { import {
lineSegment, lineSegment,
pointFrom, pointFrom,
polygonIncludesPoint,
pointOnLineSegment,
type GlobalPoint, type GlobalPoint,
type LocalPoint,
type Polygon,
vectorCross,
vectorFromPoint, vectorFromPoint,
vectorNormalize,
vectorScale,
pointFromVector,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { intersectElementWithLineSegment } from "@excalidraw/element/collision"; import { intersectElementWithLineSegment } from "@excalidraw/element/collision";
@ -16,16 +14,17 @@ import { elementCenterPoint } from "@excalidraw/common";
import { distanceToElement } from "@excalidraw/element/distance"; import { distanceToElement } from "@excalidraw/element/distance";
import { isLinearElement } from "@excalidraw/excalidraw"; import { getCommonBounds, isLinearElement } from "@excalidraw/excalidraw";
import { isFreeDrawElement } from "@excalidraw/element/typeChecks"; import { isFreeDrawElement } from "@excalidraw/element/typeChecks";
import { isPathALoop } from "@excalidraw/element/shapes"; import { isPathALoop } from "@excalidraw/element/shapes";
import {
debugDrawLine,
debugDrawPoint,
} from "@excalidraw/excalidraw/visualdebug";
import type { ExcalidrawElement } from "@excalidraw/element/types"; 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 // check if the given point is considered on the given shape's border
export const isPointOnShape = ( export const isPointOnShape = (
point: GlobalPoint, point: GlobalPoint,
@ -44,15 +43,33 @@ export const isPointInShape = (
) => { ) => {
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
if (isPathALoop(element.points)) { if (isPathALoop(element.points)) {
// for a closed path, we need to check if the point is inside the path const [minX, minY, maxX, maxY] = getCommonBounds([element]);
const r = isPointInClosedPath( const center = pointFrom<GlobalPoint>(
element.points.map((p) => (maxX + minX) / 2,
pointFrom<GlobalPoint>(element.x + p[0], element.y + p[1]), (maxY + minY) / 2,
),
point,
); );
//console.log(r); const otherPoint = pointFromVector(
return r; 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 // There isn't any "inside" for a non-looping path
@ -66,91 +83,3 @@ export const isPointInShape = (
return intersections.length === 0; 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 extends GlobalPoint | LocalPoint>(
point: Point,
bounds: Polygon<Point>,
) => {
return polygonIncludesPoint(point, bounds);
};
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
curve: Curve<Point>,
) => {
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 = <Point extends LocalPoint | GlobalPoint>(
curve: Curve<Point>,
segments = 10,
): Polyline<Point> => {
const equation = cubicBezierEquation(curve);
let startingPoint = [equation(0, 0), equation(0, 1)] as Point;
const lineSegments: Polyline<Point> = [];
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 extends LocalPoint | GlobalPoint>(
point: Point,
curve: Curve<Point>,
threshold: number,
) => {
return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
};
export const pointOnPolyline = <Point extends LocalPoint | GlobalPoint>(
point: Point,
polyline: Polyline<Point>,
threshold = 10e-5,
) => {
return polyline.some((line) => pointOnLineSegment(point, line, threshold));
};