Rectangle and line segment collision + refactoring

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs 2024-07-26 18:43:29 +02:00
parent de43cfd3ce
commit 18f3ff27a0
No known key found for this signature in database
5 changed files with 149 additions and 109 deletions

View File

@ -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];

View File

@ -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 [

View File

@ -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],
]);
});
});

View File

@ -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);

View File

@ -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