Rectangle and line segment collision + refactoring
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
de43cfd3ce
commit
18f3ff27a0
|
@ -1,4 +1,3 @@
|
|||
import { type LineSegment } from "../../utils";
|
||||
import { cross } from "../../utils/geometry/geometry";
|
||||
import BinaryHeap from "../binaryheap";
|
||||
import type { Heading } from "../math";
|
||||
|
@ -9,14 +8,12 @@ import {
|
|||
HEADING_UP,
|
||||
PointInTriangle,
|
||||
aabbForElement,
|
||||
addVectors,
|
||||
arePointsEqual,
|
||||
pointInsideBounds,
|
||||
pointToVector,
|
||||
rotatePoint,
|
||||
scalePointFromOrigin,
|
||||
scaleVector,
|
||||
subtractVectors,
|
||||
translatePoint,
|
||||
vectorToHeading,
|
||||
} from "../math";
|
||||
|
@ -692,35 +689,6 @@ const getDonglePosition = (
|
|||
return [bounds[0], point[1]];
|
||||
};
|
||||
|
||||
export const segmentsIntersectAt = (
|
||||
a: Readonly<LineSegment>,
|
||||
b: Readonly<LineSegment>,
|
||||
): Point | null => {
|
||||
const r = subtractVectors(a[1], a[0]);
|
||||
const s = subtractVectors(b[1], b[0]);
|
||||
const denominator = crossProduct(r, s);
|
||||
|
||||
if (denominator === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const i = subtractVectors(b[0], a[0]);
|
||||
const u = crossProduct(i, r) / denominator;
|
||||
const t = crossProduct(i, s) / denominator;
|
||||
|
||||
if (u === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const p = addVectors(a[0], scaleVector(r, t));
|
||||
|
||||
if (t > 0 && t < 1 && u > 0 && u < 1) {
|
||||
return p;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const crossProduct = (a: Point, b: Point): number =>
|
||||
a[0] * b[1] - a[1] * b[0];
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { Bounds } from "../excalidraw/element/bounds";
|
||||
import type { Point } from "../excalidraw/types";
|
||||
|
||||
export type LineSegment = [Point, Point];
|
||||
import type { LineSegment } from "./geometry/shape";
|
||||
|
||||
export function getBBox(line: LineSegment): Bounds {
|
||||
return [
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
interceptPointsOfLineAndEllipse,
|
||||
interceptPointsOfSegmentAndPolygon,
|
||||
lineIntersectsLine,
|
||||
lineRotate,
|
||||
pointInEllipse,
|
||||
|
@ -12,8 +13,17 @@ import {
|
|||
pointOnPolyline,
|
||||
pointRightofLine,
|
||||
pointRotate,
|
||||
segmentsIntersectAt,
|
||||
} from "./geometry";
|
||||
import type { Curve, Ellipse, Line, Point, Polygon, Polyline } from "./shape";
|
||||
import type {
|
||||
Curve,
|
||||
Ellipse,
|
||||
Line,
|
||||
LineSegment,
|
||||
Point,
|
||||
Polygon,
|
||||
Polyline,
|
||||
} from "./shape";
|
||||
|
||||
describe("point and line", () => {
|
||||
const line: Line = [
|
||||
|
@ -254,82 +264,93 @@ describe("line intersects ellipse", () => {
|
|||
expect(
|
||||
interceptPointsOfLineAndEllipse(
|
||||
{
|
||||
type: "ellipse",
|
||||
id: "test-01",
|
||||
x: -5,
|
||||
y: -5,
|
||||
strokeColor: "red",
|
||||
backgroundColor: "black",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 0,
|
||||
strokeStyle: "solid",
|
||||
roundness: null,
|
||||
roughness: 0,
|
||||
opacity: 0,
|
||||
width: 10,
|
||||
height: 25,
|
||||
center: [10, 10],
|
||||
angle: 0,
|
||||
seed: 0,
|
||||
version: 0,
|
||||
versionNonce: 0,
|
||||
index: null,
|
||||
isDeleted: false,
|
||||
groupIds: [],
|
||||
frameId: null,
|
||||
boundElements: null,
|
||||
updated: 0,
|
||||
link: null,
|
||||
locked: false,
|
||||
},
|
||||
halfWidth: 5,
|
||||
halfHeight: 10,
|
||||
} as Ellipse,
|
||||
[
|
||||
[-10, 0],
|
||||
[10, 0],
|
||||
],
|
||||
),
|
||||
[-10, 5],
|
||||
[30, 5],
|
||||
] as LineSegment,
|
||||
).map((point) => point.map(Math.round)),
|
||||
).toEqual([
|
||||
[-3.999999999999999, 0],
|
||||
[4, 0],
|
||||
[6, 5],
|
||||
[14, 5],
|
||||
]);
|
||||
});
|
||||
it("can detect two intersection points when ellipse is rotated", () => {
|
||||
expect(
|
||||
interceptPointsOfLineAndEllipse(
|
||||
{
|
||||
type: "ellipse",
|
||||
id: "test-01",
|
||||
x: -5,
|
||||
y: -5,
|
||||
strokeColor: "red",
|
||||
backgroundColor: "black",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 0,
|
||||
strokeStyle: "solid",
|
||||
roundness: null,
|
||||
roughness: 0,
|
||||
opacity: 0,
|
||||
width: 10,
|
||||
height: 25,
|
||||
center: [10, 10],
|
||||
angle: 15,
|
||||
seed: 0,
|
||||
version: 0,
|
||||
versionNonce: 0,
|
||||
index: null,
|
||||
isDeleted: false,
|
||||
groupIds: [],
|
||||
frameId: null,
|
||||
boundElements: null,
|
||||
updated: 0,
|
||||
link: null,
|
||||
locked: false,
|
||||
},
|
||||
halfWidth: 5,
|
||||
halfHeight: 10,
|
||||
} as Ellipse,
|
||||
[
|
||||
[-10, 0],
|
||||
[10, 0],
|
||||
],
|
||||
),
|
||||
[-10, 5],
|
||||
[30, 5],
|
||||
] as LineSegment,
|
||||
).map((point) => point.map(Math.round)),
|
||||
).toEqual([
|
||||
[1.9335164187732106, 19.027552390450893],
|
||||
[-4.353996644614238, 13.645482700065134],
|
||||
[12, 19],
|
||||
[5, 12],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("line segments intersection", () => {
|
||||
it("correctly detects intersection point", () => {
|
||||
expect(
|
||||
segmentsIntersectAt(
|
||||
[
|
||||
[-10, -10],
|
||||
[10, 10],
|
||||
],
|
||||
[
|
||||
[-10, 10],
|
||||
[10, -10],
|
||||
],
|
||||
),
|
||||
).toEqual([0, 0]);
|
||||
});
|
||||
|
||||
it("can detect if segments do not intersect", () => {
|
||||
expect(
|
||||
segmentsIntersectAt(
|
||||
[
|
||||
[-10, -10],
|
||||
[-5, 5],
|
||||
],
|
||||
[
|
||||
[10, -10],
|
||||
[5, 5],
|
||||
],
|
||||
),
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can detect line segment intersection with polygon", () => {
|
||||
it("can determine intercept point of a line segment and a polygon", () => {
|
||||
expect(
|
||||
interceptPointsOfSegmentAndPolygon(
|
||||
[
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
[10, 10],
|
||||
[0, 10],
|
||||
[0, 0],
|
||||
],
|
||||
[
|
||||
[-5, 5],
|
||||
[35, 5],
|
||||
],
|
||||
).map((point) => point.map(Math.round)),
|
||||
).toEqual([
|
||||
[10, 5],
|
||||
[0, 5],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import type { ExcalidrawEllipseElement } from "../../excalidraw/element/types";
|
||||
import { crossProduct } from "..";
|
||||
import {
|
||||
addVectors,
|
||||
distance2d,
|
||||
dotProduct,
|
||||
pointToVector,
|
||||
rotatePoint,
|
||||
scaleVector,
|
||||
subtractVectors,
|
||||
} from "../../excalidraw/math";
|
||||
import type {
|
||||
Point,
|
||||
|
@ -14,6 +17,7 @@ import type {
|
|||
Polycurve,
|
||||
Polyline,
|
||||
Vector,
|
||||
LineSegment,
|
||||
} from "./shape";
|
||||
|
||||
const DEFAULT_THRESHOLD = 10e-5;
|
||||
|
@ -979,20 +983,19 @@ export const pointInEllipse = (point: Point, ellipse: Ellipse) => {
|
|||
* ellipse.
|
||||
*/
|
||||
export const interceptPointsOfLineAndEllipse = (
|
||||
ellipse: ExcalidrawEllipseElement,
|
||||
ellipse: Ellipse,
|
||||
line: Line,
|
||||
): Point[] => {
|
||||
const rx = ellipse.width / 2;
|
||||
const ry = ellipse.height / 2;
|
||||
const center = [ellipse.x + rx, ellipse.y + ry] as Point;
|
||||
const rx = ellipse.halfWidth;
|
||||
const ry = ellipse.halfHeight;
|
||||
const nonRotatedLine = [
|
||||
rotatePoint(line[0], center, -ellipse.angle),
|
||||
rotatePoint(line[1], center, -ellipse.angle),
|
||||
rotatePoint(line[0], ellipse.center, -ellipse.angle),
|
||||
rotatePoint(line[1], ellipse.center, -ellipse.angle),
|
||||
] as Line;
|
||||
const dir = pointToVector(nonRotatedLine[1], nonRotatedLine[0]);
|
||||
const diff = [
|
||||
nonRotatedLine[0][0] - center[0],
|
||||
nonRotatedLine[0][1] - center[1],
|
||||
nonRotatedLine[0][0] - ellipse.center[0],
|
||||
nonRotatedLine[0][1] - ellipse.center[1],
|
||||
] as Vector;
|
||||
const mDir = [dir[0] / (rx * rx), dir[1] / (ry * ry)] as Vector;
|
||||
const mDiff = [diff[0] / (rx * rx), diff[1] / (ry * ry)] as Vector;
|
||||
|
@ -1039,3 +1042,49 @@ export const interceptPointsOfLineAndEllipse = (
|
|||
|
||||
return intersections;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the point two line segments with a definite start and end point
|
||||
* intersect at.
|
||||
*/
|
||||
export const segmentsIntersectAt = (
|
||||
a: Readonly<LineSegment>,
|
||||
b: Readonly<LineSegment>,
|
||||
): Point | null => {
|
||||
const r = subtractVectors(a[1], a[0]);
|
||||
const s = subtractVectors(b[1], b[0]);
|
||||
const denominator = crossProduct(r, s);
|
||||
|
||||
if (denominator === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const i = subtractVectors(b[0], a[0]);
|
||||
const u = crossProduct(i, r) / denominator;
|
||||
const t = crossProduct(i, s) / denominator;
|
||||
|
||||
if (u === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const p = addVectors(a[0], scaleVector(r, t));
|
||||
|
||||
if (t > 0 && t < 1 && u > 0 && u < 1) {
|
||||
return p;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const interceptPointsOfSegmentAndPolygon = (
|
||||
polygon: Readonly<Polygon>,
|
||||
segment: Readonly<LineSegment>,
|
||||
) =>
|
||||
polygon
|
||||
.reduce((segments, current, idx, poly) => {
|
||||
return idx === 0
|
||||
? []
|
||||
: ([...segments, [poly[idx - 1] as Point, current]] as LineSegment[]);
|
||||
}, [] as LineSegment[])
|
||||
.map((s) => segmentsIntersectAt(s, segment))
|
||||
.filter((point) => point !== null);
|
||||
|
|
|
@ -36,9 +36,12 @@ import type { Drawable, Op } from "roughjs/bin/core";
|
|||
export type Point = [number, number];
|
||||
export type Vector = Point;
|
||||
|
||||
// a line (segment) is defined by two endpoints
|
||||
// a line is defined by two endpoints
|
||||
export type Line = [Point, Point];
|
||||
|
||||
// a line segment with a definite start and end points
|
||||
export type LineSegment = [Point, Point];
|
||||
|
||||
// a polyline (made up term here) is a line consisting of other line segments
|
||||
// this corresponds to a straight line element in the editor but it could also
|
||||
// be used to model other elements
|
||||
|
|
Loading…
Reference in New Issue