Refactoring bounds, arrows, bboxes

This commit is contained in:
Mark Tolmacs 2024-09-23 19:43:22 +02:00
parent 9eb08df3ea
commit dff60e6f6f
No known key found for this signature in database
10 changed files with 318 additions and 408 deletions

View file

@ -0,0 +1,178 @@
import type { Drawable } from "roughjs/bin/core";
import {
degrees,
degreesToRadians,
point,
pointFromArray,
pointRotateRads,
radians,
type Degrees,
} from "../../math";
import type { Arrowhead, ExcalidrawLinearElement } from "./types";
import { getCurvePathOps } from "../../utils/geometry/shape";
import { invariant } from "../utils";
/** @returns number in degrees */
const getArrowheadAngle = (arrowhead: Arrowhead): Degrees => {
switch (arrowhead) {
case "bar":
return degrees(90);
case "arrow":
return degrees(20);
default:
return degrees(25);
}
};
/** @returns number in pixels */
const getArrowheadSize = (arrowhead: Arrowhead): number => {
switch (arrowhead) {
case "arrow":
return 25;
case "diamond":
case "diamond_outline":
return 12;
default:
return 15;
}
};
export const getArrowheadPoints = (
element: ExcalidrawLinearElement,
shape: Drawable[],
position: "start" | "end",
arrowhead: Arrowhead,
) => {
const ops = getCurvePathOps(shape[0]);
if (ops.length < 1) {
return null;
}
// The index of the bCurve operation to examine.
const index = position === "start" ? 1 : ops.length - 1;
const data = ops[index].data;
invariant(data.length === 6, "Op data length is not 6");
const p3 = point(data[4], data[5]);
const p2 = point(data[2], data[3]);
const p1 = point(data[0], data[1]);
// We need to find p0 of the bezier curve.
// It is typically the last point of the previous
// curve; it can also be the position of moveTo operation.
const prevOp = ops[index - 1];
let p0 = point(0, 0);
if (prevOp.op === "move") {
const p = pointFromArray(prevOp.data);
invariant(p != null, "Op data is not a point");
p0 = p;
} else if (prevOp.op === "bcurveTo") {
p0 = point(prevOp.data[4], prevOp.data[5]);
}
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
const equation = (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);
// Ee know the last point of the arrow (or the first, if start arrowhead).
const [x2, y2] = position === "start" ? p0 : p3;
// By using cubic bezier equation (B(t)) and the given parameters,
// we calculate a point that is closer to the last point.
// The value 0.3 is chosen arbitrarily and it works best for all
// the tested cases.
const [x1, y1] = [equation(0.3, 0), equation(0.3, 1)];
// Find the normalized direction vector based on the
// previously calculated points.
const distance = Math.hypot(x2 - x1, y2 - y1);
const nx = (x2 - x1) / distance;
const ny = (y2 - y1) / distance;
const size = getArrowheadSize(arrowhead);
let length = 0;
{
// Length for -> arrows is based on the length of the last section
const [cx, cy] =
position === "end"
? element.points[element.points.length - 1]
: element.points[0];
const [px, py] =
element.points.length > 1
? position === "end"
? element.points[element.points.length - 2]
: element.points[1]
: [0, 0];
length = Math.hypot(cx - px, cy - py);
}
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
const lengthMultiplier =
arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5;
const minSize = Math.min(size, length * lengthMultiplier);
const xs = x2 - nx * minSize;
const ys = y2 - ny * minSize;
if (
arrowhead === "dot" ||
arrowhead === "circle" ||
arrowhead === "circle_outline"
) {
const diameter = Math.hypot(ys - y2, xs - x2) + element.strokeWidth - 2;
return [x2, y2, diameter];
}
const angle = getArrowheadAngle(arrowhead);
// Return points
const [x3, y3] = pointRotateRads(
point(xs, ys),
point(x2, y2),
radians((-angle * Math.PI) / 180),
);
const [x4, y4] = pointRotateRads(
point(xs, ys),
point(x2, y2),
degreesToRadians(angle),
);
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
// point opposite to the arrowhead point
let ox;
let oy;
if (position === "start") {
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
[ox, oy] = pointRotateRads(
point(x2 + minSize * 2, y2),
point(x2, y2),
radians(Math.atan2(py - y2, px - x2)),
);
} else {
const [px, py] =
element.points.length > 1
? element.points[element.points.length - 2]
: [0, 0];
[ox, oy] = pointRotateRads(
point(x2 - minSize * 2, y2),
point(x2, y2),
radians(Math.atan2(y2 - py, x2 - px)),
);
}
return [x2, y2, x3, y3, ox, oy, x4, y4];
}
return [x2, y2, x3, y3, x4, y4];
};

View file

@ -1,7 +1,6 @@
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
Arrowhead,
ExcalidrawFreeDrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
@ -23,16 +22,8 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { ShapeCache } from "../scene/ShapeCache";
import { arrayToMap, invariant } from "../utils";
import type {
Degrees,
GlobalPoint,
LineSegment,
LocalPoint,
Radians,
} from "../../math";
import type { GlobalPoint, LocalPoint } from "../../math";
import {
degreesToRadians,
lineSegment,
point,
pointDistance,
pointFromArray,
@ -40,14 +31,7 @@ import {
pointRescaleFromTopLeft,
} from "../../math";
import type { Mutable } from "../utility-types";
export type RectangleBox = {
x: number;
y: number;
width: number;
height: number;
angle: number;
};
import { getCurvePathOps } from "../../utils/geometry/shape";
type MaybeQuadraticSolution = [number | null, number | null] | false;
@ -68,7 +52,7 @@ export type SceneBounds = readonly [
sceneY2: number,
];
export class ElementBounds {
class ElementBounds {
private static boundsCache = new WeakMap<
ExcalidrawElement,
{
@ -241,117 +225,6 @@ export const getElementAbsoluteCoords = (
];
};
/*
* for a given element, `getElementLineSegments` returns line segments
* that can be used for visual collision detection (useful for frames)
* as opposed to bounding box collision detection
*/
export const getElementLineSegments = (
element: ExcalidrawElement,
elementsMap: ElementsMap,
): LineSegment<GlobalPoint>[] => {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
const center: GlobalPoint = point(cx, cy);
if (isLinearElement(element) || isFreeDrawElement(element)) {
const segments: LineSegment<GlobalPoint>[] = [];
let i = 0;
while (i < element.points.length - 1) {
segments.push(
lineSegment(
pointRotateRads(
point(
element.points[i][0] + element.x,
element.points[i][1] + element.y,
),
center,
element.angle,
),
pointRotateRads(
point(
element.points[i + 1][0] + element.x,
element.points[i + 1][1] + element.y,
),
center,
element.angle,
),
),
);
i++;
}
return segments;
}
const [nw, ne, sw, se, n, s, w, e] = (
[
[x1, y1],
[x2, y1],
[x1, y2],
[x2, y2],
[cx, y1],
[cx, y2],
[x1, cy],
[x2, cy],
] as GlobalPoint[]
).map((point) => pointRotateRads(point, center, element.angle));
if (element.type === "diamond") {
return [
lineSegment(n, w),
lineSegment(n, e),
lineSegment(s, w),
lineSegment(s, e),
];
}
if (element.type === "ellipse") {
return [
lineSegment(n, w),
lineSegment(n, e),
lineSegment(s, w),
lineSegment(s, e),
lineSegment(n, w),
lineSegment(n, e),
lineSegment(s, w),
lineSegment(s, e),
];
}
return [
lineSegment(nw, ne),
lineSegment(sw, se),
lineSegment(nw, sw),
lineSegment(ne, se),
lineSegment(nw, e),
lineSegment(sw, e),
lineSegment(ne, w),
lineSegment(se, w),
];
};
/**
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
*
* Rectangle here means any rectangular frame, not an excalidraw element.
*/
export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => {
return [
boxSceneCoords.x,
boxSceneCoords.y,
boxSceneCoords.x + boxSceneCoords.width,
boxSceneCoords.y + boxSceneCoords.height,
boxSceneCoords.x + boxSceneCoords.width / 2,
boxSceneCoords.y + boxSceneCoords.height / 2,
];
};
export const getDiamondPoints = (element: ExcalidrawElement) => {
// Here we add +1 to avoid these numbers to be 0
// otherwise rough.js will throw an error complaining about it
@ -367,15 +240,6 @@ export const getDiamondPoints = (element: ExcalidrawElement) => {
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
};
export const getCurvePathOps = (shape: Drawable): Op[] => {
for (const set of shape.sets) {
if (set.type === "path") {
return set.ops;
}
}
return shape.sets[0].ops;
};
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
const getBezierValueForT = (
t: number,
@ -548,171 +412,6 @@ const getFreeDrawElementAbsoluteCoords = (
return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2];
};
/** @returns number in pixels */
export const getArrowheadSize = (arrowhead: Arrowhead): number => {
switch (arrowhead) {
case "arrow":
return 25;
case "diamond":
case "diamond_outline":
return 12;
default:
return 15;
}
};
/** @returns number in degrees */
export const getArrowheadAngle = (arrowhead: Arrowhead): Degrees => {
switch (arrowhead) {
case "bar":
return 90 as Degrees;
case "arrow":
return 20 as Degrees;
default:
return 25 as Degrees;
}
};
export const getArrowheadPoints = (
element: ExcalidrawLinearElement,
shape: Drawable[],
position: "start" | "end",
arrowhead: Arrowhead,
) => {
const ops = getCurvePathOps(shape[0]);
if (ops.length < 1) {
return null;
}
// The index of the bCurve operation to examine.
const index = position === "start" ? 1 : ops.length - 1;
const data = ops[index].data;
invariant(data.length === 6, "Op data length is not 6");
const p3 = point(data[4], data[5]);
const p2 = point(data[2], data[3]);
const p1 = point(data[0], data[1]);
// We need to find p0 of the bezier curve.
// It is typically the last point of the previous
// curve; it can also be the position of moveTo operation.
const prevOp = ops[index - 1];
let p0 = point(0, 0);
if (prevOp.op === "move") {
const p = pointFromArray(prevOp.data);
invariant(p != null, "Op data is not a point");
p0 = p;
} else if (prevOp.op === "bcurveTo") {
p0 = point(prevOp.data[4], prevOp.data[5]);
}
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
const equation = (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);
// Ee know the last point of the arrow (or the first, if start arrowhead).
const [x2, y2] = position === "start" ? p0 : p3;
// By using cubic bezier equation (B(t)) and the given parameters,
// we calculate a point that is closer to the last point.
// The value 0.3 is chosen arbitrarily and it works best for all
// the tested cases.
const [x1, y1] = [equation(0.3, 0), equation(0.3, 1)];
// Find the normalized direction vector based on the
// previously calculated points.
const distance = Math.hypot(x2 - x1, y2 - y1);
const nx = (x2 - x1) / distance;
const ny = (y2 - y1) / distance;
const size = getArrowheadSize(arrowhead);
let length = 0;
{
// Length for -> arrows is based on the length of the last section
const [cx, cy] =
position === "end"
? element.points[element.points.length - 1]
: element.points[0];
const [px, py] =
element.points.length > 1
? position === "end"
? element.points[element.points.length - 2]
: element.points[1]
: [0, 0];
length = Math.hypot(cx - px, cy - py);
}
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
const lengthMultiplier =
arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5;
const minSize = Math.min(size, length * lengthMultiplier);
const xs = x2 - nx * minSize;
const ys = y2 - ny * minSize;
if (
arrowhead === "dot" ||
arrowhead === "circle" ||
arrowhead === "circle_outline"
) {
const diameter = Math.hypot(ys - y2, xs - x2) + element.strokeWidth - 2;
return [x2, y2, diameter];
}
const angle = getArrowheadAngle(arrowhead);
// Return points
const [x3, y3] = pointRotateRads(
point(xs, ys),
point(x2, y2),
((-angle * Math.PI) / 180) as Radians,
);
const [x4, y4] = pointRotateRads(
point(xs, ys),
point(x2, y2),
degreesToRadians(angle),
);
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
// point opposite to the arrowhead point
let ox;
let oy;
if (position === "start") {
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
[ox, oy] = pointRotateRads(
point(x2 + minSize * 2, y2),
point(x2, y2),
Math.atan2(py - y2, px - x2) as Radians,
);
} else {
const [px, py] =
element.points.length > 1
? element.points[element.points.length - 2]
: [0, 0];
[ox, oy] = pointRotateRads(
point(x2 - minSize * 2, y2),
point(x2, y2),
Math.atan2(y2 - py, x2 - px) as Radians,
);
}
return [x2, y2, x3, y3, ox, oy, x4, y4];
}
return [x2, y2, x3, y3, x4, y4];
};
const generateLinearElementShape = (
element: ExcalidrawLinearElement,
): Drawable => {

View file

@ -20,7 +20,6 @@ export {
getElementBounds,
getCommonBounds,
getDiamondPoints,
getArrowheadPoints,
getClosestElementBounds,
} from "./bounds";
@ -55,6 +54,7 @@ export {
getNormalizedDimensions,
} from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions";
export { getArrowheadPoints } from "./arrow";
/**
* @deprecated unsafe, use hashElementsVersion instead

View file

@ -13,11 +13,7 @@ import type {
} from "./types";
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
import type { Bounds } from "./bounds";
import {
getCurvePathOps,
getElementPointsCoords,
getMinMaxXYFromCurvePathOps,
} from "./bounds";
import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
import type {
AppState,
InteractiveCanvasAppState,
@ -66,6 +62,7 @@ import {
mapIntervalToBezierT,
} from "../shapes";
import { getGridPoint } from "../snapping";
import { getCurvePathOps } from "../../utils/geometry/shape";
const editorMidPointsCache: {
version: number | null;

View file

@ -25,12 +25,22 @@ import type {
import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
import { getElementLineSegments } from "./element/bounds";
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
import { elementsOverlappingBBox } from "../utils/";
import {
isFrameElement,
isFrameLikeElement,
isFreeDrawElement,
isLinearElement,
} from "./element/typeChecks";
import type { ReadonlySetLike } from "./utility-types";
import type { GlobalPoint } from "../math";
import { isPointWithinBounds, point } from "../math";
import type { GlobalPoint, LineSegment } from "../math";
import {
isPointWithinBounds,
lineSegment,
point,
pointRotateRads,
segmentsIntersectAt,
} from "../math";
// --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = (
@ -64,6 +74,101 @@ export const bindElementsToFramesAfterDuplication = (
}
};
/*
* for a given element, `getElementLineSegments` returns line segments
* that can be used for visual collision detection (useful for frames)
* as opposed to bounding box collision detection
*/
const getElementLineSegments = (
element: ExcalidrawElement,
elementsMap: ElementsMap,
): LineSegment<GlobalPoint>[] => {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
const center: GlobalPoint = point(cx, cy);
if (isLinearElement(element) || isFreeDrawElement(element)) {
const segments: LineSegment<GlobalPoint>[] = [];
let i = 0;
while (i < element.points.length - 1) {
segments.push(
lineSegment(
pointRotateRads(
point(
element.points[i][0] + element.x,
element.points[i][1] + element.y,
),
center,
element.angle,
),
pointRotateRads(
point(
element.points[i + 1][0] + element.x,
element.points[i + 1][1] + element.y,
),
center,
element.angle,
),
),
);
i++;
}
return segments;
}
const [nw, ne, sw, se, n, s, w, e] = (
[
[x1, y1],
[x2, y1],
[x1, y2],
[x2, y2],
[cx, y1],
[cx, y2],
[x1, cy],
[x2, cy],
] as GlobalPoint[]
).map((point) => pointRotateRads(point, center, element.angle));
if (element.type === "diamond") {
return [
lineSegment(n, w),
lineSegment(n, e),
lineSegment(s, w),
lineSegment(s, e),
];
}
if (element.type === "ellipse") {
return [
lineSegment(n, w),
lineSegment(n, e),
lineSegment(s, w),
lineSegment(s, e),
lineSegment(n, w),
lineSegment(n, e),
lineSegment(s, w),
lineSegment(s, e),
];
}
return [
lineSegment(nw, ne),
lineSegment(sw, se),
lineSegment(nw, sw),
lineSegment(ne, se),
lineSegment(nw, e),
lineSegment(sw, e),
lineSegment(ne, w),
lineSegment(se, w),
];
};
export function isElementIntersectingFrame(
element: ExcalidrawElement,
frame: ExcalidrawFrameLikeElement,
@ -75,7 +180,7 @@ export function isElementIntersectingFrame(
const intersecting = frameLineSegments.some((frameLineSegment) =>
elementLineSegments.some((elementLineSegment) =>
doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
segmentsIntersectAt(frameLineSegment, elementLineSegment),
),
);

View file

@ -1,7 +1,7 @@
import type { Point as RoughPoint } from "roughjs/bin/geometry";
import type { Drawable, Options } from "roughjs/bin/core";
import type { RoughGenerator } from "roughjs/bin/generator";
import { getDiamondPoints, getArrowheadPoints } from "../element";
import { getArrowheadPoints, getDiamondPoints } from "../element";
import type { ElementShapes } from "./types";
import type {
ExcalidrawElement,

View file

@ -0,0 +1,22 @@
import { point } from "./point";
import { lineSegment, segmentsIntersectAt } from "./segment";
import type { GlobalPoint, LineSegment } from "./types";
describe("segment intersects segment", () => {
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
it("intersection", () => {
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
});
});

View file

@ -1,71 +0,0 @@
import type { LineSegment } from "../math";
import {
vectorCross,
vectorFromPoint,
type GlobalPoint,
type LocalPoint,
} from "../math";
import type { Bounds } from "../excalidraw/element/bounds";
export function getBBox<P extends LocalPoint | GlobalPoint>(
line: LineSegment<P>,
): Bounds {
return [
Math.min(line[0][0], line[1][0]),
Math.min(line[0][1], line[1][1]),
Math.max(line[0][0], line[1][0]),
Math.max(line[0][1], line[1][1]),
];
}
export function doBBoxesIntersect(a: Bounds, b: Bounds) {
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
}
const EPSILON = 0.000001;
export function isPointOnLine<P extends GlobalPoint | LocalPoint>(
l: LineSegment<P>,
p: P,
) {
const p1 = vectorFromPoint(l[1], l[0]);
const p2 = vectorFromPoint(p, l[0]);
const r = vectorCross(p1, p2);
return Math.abs(r) < EPSILON;
}
export function isPointRightOfLine<P extends GlobalPoint | LocalPoint>(
l: LineSegment<P>,
p: P,
) {
const p1 = vectorFromPoint(l[1], l[0]);
const p2 = vectorFromPoint(p, l[0]);
return vectorCross(p1, p2) < 0;
}
export function isLineSegmentTouchingOrCrossingLine<
P extends GlobalPoint | LocalPoint,
>(a: LineSegment<P>, b: LineSegment<P>) {
return (
isPointOnLine(a, b[0]) ||
isPointOnLine(a, b[1]) ||
(isPointRightOfLine(a, b[0])
? !isPointRightOfLine(a, b[1])
: isPointRightOfLine(a, b[1]))
);
}
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
export function doLineSegmentsIntersect<P extends GlobalPoint | LocalPoint>(
a: LineSegment<P>,
b: LineSegment<P>,
) {
return (
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
isLineSegmentTouchingOrCrossingLine(a, b) &&
isLineSegmentTouchingOrCrossingLine(b, a)
);
}

View file

@ -89,22 +89,3 @@ describe("point and ellipse", () => {
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
});
});
describe("line and line", () => {
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
it("intersection", () => {
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
});
});

View file

@ -1,4 +1,3 @@
export * from "./export";
export * from "./withinBounds";
export * from "./bbox";
export { getCommonBounds } from "../excalidraw/element/bounds";