[skip ci] inverted polygon hit test
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
db3e5c63ef
commit
6c93d6e997
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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));
|
|
||||||
};
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user