mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
feat: Precise hit testing (#9488)
This commit is contained in:
@ -1,135 +0,0 @@
|
||||
import {
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
polygonIncludesPoint,
|
||||
pointOnLineSegment,
|
||||
pointOnPolygon,
|
||||
polygonFromPoints,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
type Polygon,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { Curve } from "@excalidraw/math";
|
||||
|
||||
import { pointInEllipse, pointOnEllipse } from "./shape";
|
||||
|
||||
import type { Polycurve, Polyline, GeometricShape } from "./shape";
|
||||
|
||||
// check if the given point is considered on the given shape's border
|
||||
export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
shape: GeometricShape<Point>,
|
||||
tolerance = 0,
|
||||
) => {
|
||||
// get the distance from the given point to the given element
|
||||
// check if the distance is within the given epsilon range
|
||||
switch (shape.type) {
|
||||
case "polygon":
|
||||
return pointOnPolygon(point, shape.data, tolerance);
|
||||
case "ellipse":
|
||||
return pointOnEllipse(point, shape.data, tolerance);
|
||||
case "line":
|
||||
return pointOnLineSegment(point, shape.data, tolerance);
|
||||
case "polyline":
|
||||
return pointOnPolyline(point, shape.data, tolerance);
|
||||
case "curve":
|
||||
return pointOnCurve(point, shape.data, tolerance);
|
||||
case "polycurve":
|
||||
return pointOnPolycurve(point, shape.data, tolerance);
|
||||
default:
|
||||
throw Error(`shape ${shape} is not implemented`);
|
||||
}
|
||||
};
|
||||
|
||||
// check if the given point is considered inside the element's border
|
||||
export const isPointInShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
shape: GeometricShape<Point>,
|
||||
) => {
|
||||
switch (shape.type) {
|
||||
case "polygon":
|
||||
return polygonIncludesPoint(point, shape.data);
|
||||
case "line":
|
||||
return false;
|
||||
case "curve":
|
||||
return false;
|
||||
case "ellipse":
|
||||
return pointInEllipse(point, shape.data);
|
||||
case "polyline": {
|
||||
const polygon = polygonFromPoints(shape.data.flat());
|
||||
return polygonIncludesPoint(point, polygon);
|
||||
}
|
||||
case "polycurve": {
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
throw Error(`shape ${shape} is not implemented`);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
polycurve: Polycurve<Point>,
|
||||
tolerance: number,
|
||||
) => {
|
||||
return polycurve.some((curve) => pointOnCurve(point, curve, tolerance));
|
||||
};
|
||||
|
||||
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));
|
||||
};
|
@ -24,7 +24,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
|
@ -1,90 +0,0 @@
|
||||
import {
|
||||
curve,
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
lineSegmentRotate,
|
||||
pointFrom,
|
||||
pointRotateDegs,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { Curve, Degrees, GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import { pointOnCurve, pointOnPolyline } from "../src/collision";
|
||||
|
||||
import type { Polyline } from "../src/shape";
|
||||
|
||||
describe("point and curve", () => {
|
||||
const c: Curve<GlobalPoint> = curve(
|
||||
pointFrom(1.4, 1.65),
|
||||
pointFrom(1.9, 7.9),
|
||||
pointFrom(5.9, 1.65),
|
||||
pointFrom(6.44, 4.84),
|
||||
);
|
||||
|
||||
it("point on curve", () => {
|
||||
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
|
||||
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
|
||||
|
||||
expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true);
|
||||
|
||||
expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and polylines", () => {
|
||||
const polyline: Polyline<GlobalPoint> = [
|
||||
lineSegment(pointFrom(1, 0), pointFrom(1, 2)),
|
||||
lineSegment(pointFrom(1, 2), pointFrom(2, 2)),
|
||||
lineSegment(pointFrom(2, 2), pointFrom(2, 1)),
|
||||
lineSegment(pointFrom(2, 1), pointFrom(3, 1)),
|
||||
];
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false);
|
||||
expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false);
|
||||
});
|
||||
|
||||
it("point on the line with rotation", () => {
|
||||
const truePoints = [
|
||||
pointFrom(1, 0),
|
||||
pointFrom(1, 2),
|
||||
pointFrom(2, 2),
|
||||
pointFrom(2, 1),
|
||||
pointFrom(3, 1),
|
||||
];
|
||||
|
||||
truePoints.forEach((p) => {
|
||||
const rotation = (Math.random() * 360) as Degrees;
|
||||
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
|
||||
const rotatedPolyline = polyline.map((line) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||
});
|
||||
|
||||
const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)];
|
||||
|
||||
falsePoints.forEach((p) => {
|
||||
const rotation = (Math.random() * 360) as Degrees;
|
||||
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
|
||||
const rotatedPolyline = polyline.map((line) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user