From dff60e6f6f1dd23650e5954f3623c7b7ed19dccd Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Mon, 23 Sep 2024 19:43:22 +0200 Subject: [PATCH] Refactoring bounds, arrows, bboxes --- packages/excalidraw/element/arrow.ts | 178 ++++++++++ packages/excalidraw/element/bounds.ts | 307 +----------------- packages/excalidraw/element/index.ts | 2 +- .../excalidraw/element/linearElementEditor.ts | 7 +- packages/excalidraw/frame.ts | 117 ++++++- packages/excalidraw/scene/Shape.ts | 2 +- packages/math/segment.test.ts | 22 ++ packages/utils/bbox.ts | 71 ---- packages/utils/geometry/geometry.test.ts | 19 -- packages/utils/index.ts | 1 - 10 files changed, 318 insertions(+), 408 deletions(-) create mode 100644 packages/excalidraw/element/arrow.ts create mode 100644 packages/math/segment.test.ts delete mode 100644 packages/utils/bbox.ts diff --git a/packages/excalidraw/element/arrow.ts b/packages/excalidraw/element/arrow.ts new file mode 100644 index 0000000000..63717ca2fd --- /dev/null +++ b/packages/excalidraw/element/arrow.ts @@ -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]; +}; diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index eb6a5036fd..5593219328 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -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[] => { - const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( - element, - elementsMap, - ); - - const center: GlobalPoint = point(cx, cy); - - if (isLinearElement(element) || isFreeDrawElement(element)) { - const segments: LineSegment[] = []; - - 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 => { diff --git a/packages/excalidraw/element/index.ts b/packages/excalidraw/element/index.ts index a9b747681f..1730b4cabb 100644 --- a/packages/excalidraw/element/index.ts +++ b/packages/excalidraw/element/index.ts @@ -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 diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index cdb1738fb5..6e1a0e58e5 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -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; diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index a9736a96ac..d91e78ae3c 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -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[] => { + const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + ); + + const center: GlobalPoint = point(cx, cy); + + if (isLinearElement(element) || isFreeDrawElement(element)) { + const segments: LineSegment[] = []; + + 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), ), ); diff --git a/packages/excalidraw/scene/Shape.ts b/packages/excalidraw/scene/Shape.ts index 192b61ef04..06c55b52ac 100644 --- a/packages/excalidraw/scene/Shape.ts +++ b/packages/excalidraw/scene/Shape.ts @@ -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, diff --git a/packages/math/segment.test.ts b/packages/math/segment.test.ts new file mode 100644 index 0000000000..74d3496f59 --- /dev/null +++ b/packages/math/segment.test.ts @@ -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 = lineSegment(point(1, 4), point(3, 4)); + const lineB: LineSegment = lineSegment(point(2, 1), point(2, 7)); + const lineC: LineSegment = lineSegment(point(1, 8), point(3, 8)); + const lineD: LineSegment = lineSegment(point(1, 8), point(3, 8)); + const lineE: LineSegment = lineSegment(point(1, 9), point(3, 9)); + const lineF: LineSegment = lineSegment(point(1, 2), point(3, 4)); + const lineG: LineSegment = 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); + }); +}); diff --git a/packages/utils/bbox.ts b/packages/utils/bbox.ts deleted file mode 100644 index a1ffb997b2..0000000000 --- a/packages/utils/bbox.ts +++ /dev/null @@ -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

( - line: LineSegment

, -): 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

( - l: LineSegment

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

( - l: LineSegment

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

, b: LineSegment

) { - 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

( - a: LineSegment

, - b: LineSegment

, -) { - return ( - doBBoxesIntersect(getBBox(a), getBBox(b)) && - isLineSegmentTouchingOrCrossingLine(a, b) && - isLineSegmentTouchingOrCrossingLine(b, a) - ); -} diff --git a/packages/utils/geometry/geometry.test.ts b/packages/utils/geometry/geometry.test.ts index 6c096074fd..241a12e870 100644 --- a/packages/utils/geometry/geometry.test.ts +++ b/packages/utils/geometry/geometry.test.ts @@ -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 = lineSegment(point(1, 4), point(3, 4)); - const lineB: LineSegment = lineSegment(point(2, 1), point(2, 7)); - const lineC: LineSegment = lineSegment(point(1, 8), point(3, 8)); - const lineD: LineSegment = lineSegment(point(1, 8), point(3, 8)); - const lineE: LineSegment = lineSegment(point(1, 9), point(3, 9)); - const lineF: LineSegment = lineSegment(point(1, 2), point(3, 4)); - const lineG: LineSegment = 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); - }); -}); diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 9ba56da871..a51b3c9cc3 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1,4 +1,3 @@ export * from "./export"; export * from "./withinBounds"; -export * from "./bbox"; export { getCommonBounds } from "../excalidraw/element/bounds";