mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-16 22:10:48 +02:00
merge
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));
|
||||
};
|
167
packages/utils/src/visualdebug.ts
Normal file
167
packages/utils/src/visualdebug.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
isLineSegment,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { isBounds } from "@excalidraw/element";
|
||||
|
||||
import type { Curve } from "@excalidraw/math";
|
||||
import type { LineSegment } from "@excalidraw/utils";
|
||||
|
||||
import type { Bounds } from "@excalidraw/element";
|
||||
|
||||
// The global data holder to collect the debug operations
|
||||
declare global {
|
||||
interface Window {
|
||||
visualDebug?: {
|
||||
data: DebugElement[][];
|
||||
currentFrame?: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type DebugElement = {
|
||||
color: string;
|
||||
data: LineSegment<GlobalPoint> | Curve<GlobalPoint>;
|
||||
permanent: boolean;
|
||||
};
|
||||
|
||||
export const debugDrawCubicBezier = (
|
||||
c: Curve<GlobalPoint>,
|
||||
opts?: {
|
||||
color?: string;
|
||||
permanent?: boolean;
|
||||
},
|
||||
) => {
|
||||
addToCurrentFrame({
|
||||
color: opts?.color ?? "purple",
|
||||
permanent: !!opts?.permanent,
|
||||
data: c,
|
||||
});
|
||||
};
|
||||
|
||||
export const debugDrawLine = (
|
||||
segment: LineSegment<GlobalPoint> | LineSegment<GlobalPoint>[],
|
||||
opts?: {
|
||||
color?: string;
|
||||
permanent?: boolean;
|
||||
},
|
||||
) => {
|
||||
const segments = (
|
||||
isLineSegment(segment) ? [segment] : segment
|
||||
) as LineSegment<GlobalPoint>[];
|
||||
|
||||
segments.forEach((data) =>
|
||||
addToCurrentFrame({
|
||||
color: opts?.color ?? "red",
|
||||
data,
|
||||
permanent: !!opts?.permanent,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const debugDrawPoint = (
|
||||
p: GlobalPoint,
|
||||
opts?: {
|
||||
color?: string;
|
||||
permanent?: boolean;
|
||||
fuzzy?: boolean;
|
||||
},
|
||||
) => {
|
||||
const xOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
||||
const yOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
||||
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10),
|
||||
pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
|
||||
),
|
||||
{
|
||||
color: opts?.color ?? "cyan",
|
||||
permanent: opts?.permanent,
|
||||
},
|
||||
);
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10),
|
||||
pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
|
||||
),
|
||||
{
|
||||
color: opts?.color ?? "cyan",
|
||||
permanent: opts?.permanent,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const debugDrawBounds = (
|
||||
box: Bounds | Bounds[],
|
||||
opts?: {
|
||||
color?: string;
|
||||
permanent?: boolean;
|
||||
},
|
||||
) => {
|
||||
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
||||
debugDrawLine(
|
||||
[
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[1]),
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[2], bbox[3]),
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
||||
),
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[3]),
|
||||
pointFrom<GlobalPoint>(bbox[0], bbox[1]),
|
||||
),
|
||||
],
|
||||
{
|
||||
color: opts?.color ?? "green",
|
||||
permanent: !!opts?.permanent,
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const debugDrawPoints = (
|
||||
{
|
||||
x,
|
||||
y,
|
||||
points,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
points: LocalPoint[];
|
||||
},
|
||||
options?: any,
|
||||
) => {
|
||||
points.forEach((p) =>
|
||||
debugDrawPoint(pointFrom<GlobalPoint>(x + p[0], y + p[1]), options),
|
||||
);
|
||||
};
|
||||
|
||||
export const debugCloseFrame = () => {
|
||||
window.visualDebug?.data.push([]);
|
||||
};
|
||||
|
||||
export const debugClear = () => {
|
||||
if (window.visualDebug?.data) {
|
||||
window.visualDebug.data = [];
|
||||
}
|
||||
};
|
||||
|
||||
const addToCurrentFrame = (element: DebugElement) => {
|
||||
if (window.visualDebug?.data && window.visualDebug.data.length === 0) {
|
||||
window.visualDebug.data[0] = [];
|
||||
}
|
||||
window.visualDebug?.data &&
|
||||
window.visualDebug.data[window.visualDebug.data.length - 1].push(element);
|
||||
};
|
@@ -25,7 +25,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -82,7 +82,6 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||
},
|
||||
"penDetected": false,
|
||||
"penMode": false,
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
|
@@ -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