Replace intersection code

pull/8539/merge^2
Mark Tolmacs 5 months ago
parent 411deae176
commit 0c02972695
No known key found for this signature in database

@ -1614,7 +1614,6 @@ export const actionChangeArrowType = register({
startGlobalPoint,
endGlobalPoint,
startHoveredElement,
elementsMap,
)
: startGlobalPoint;
const finalEndPoint = endHoveredElement
@ -1622,7 +1621,6 @@ export const actionChangeArrowType = register({
endGlobalPoint,
startGlobalPoint,
endHoveredElement,
elementsMap,
)
: endGlobalPoint;

@ -89,7 +89,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"endBinding": {
"elementId": "ellipse-1",
"fixedPoint": null,
"focus": -0.008153707962747813,
"focus": -0.008835048729392623,
"gap": 11.562288374879595,
},
"fillStyle": "solid",
@ -120,7 +120,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"startBinding": {
"elementId": "id49",
"fixedPoint": null,
"focus": -0.08139534883720931,
"focus": -0.08860759493670874,
"gap": 1,
},
"strokeColor": "#1864ab",
@ -147,7 +147,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"endBinding": {
"elementId": "ellipse-1",
"fixedPoint": null,
"focus": 0.10666666666666667,
"focus": 0.1045751633986928,
"gap": 3.8343264684446097,
},
"fillStyle": "solid",
@ -1485,7 +1485,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"endBinding": {
"elementId": "Alice",
"fixedPoint": null,
"focus": 0,
"focus": 1.7573472843231123e-16,
"gap": 5.299874999999986,
},
"fillStyle": "solid",

@ -1,18 +1,6 @@
import * as GA from "../../math/ga/ga";
import * as GAPoint from "../../math/ga/gapoints";
import * as GADirection from "../../math/ga/gadirections";
import * as GALine from "../../math/ga/galines";
import * as GATransform from "../../math/ga/gatransforms";
import type {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawRectangleElement,
ExcalidrawDiamondElement,
ExcalidrawEllipseElement,
ExcalidrawImageElement,
ExcalidrawFrameLikeElement,
ExcalidrawIframeLikeElement,
NonDeleted,
ExcalidrawLinearElement,
PointBinding,
@ -28,7 +16,7 @@ import type {
Bounds,
} from "./types";
import { getCenterForBounds, getElementAbsoluteCoords } from "./bounds";
import { getCenterForBounds } from "./bounds";
import type { AppState } from "../types";
import { isPointOnShape } from "../../utils/collision";
import { getElementAtPosition } from "../scene";
@ -41,7 +29,6 @@ import {
isFixedPointBinding,
isFrameLikeElement,
isLinearElement,
isRectangularElement,
isTextElement,
} from "./typeChecks";
import type { ElementUpdate } from "./mutateElement";
@ -69,7 +56,6 @@ import {
pointRotateRads,
type GlobalPoint,
vectorFromPoint,
pointFromPair,
pointDistanceSq,
clamp,
radians,
@ -78,10 +64,14 @@ import {
vectorRotate,
vectorNormalize,
pointDistance,
line,
lineLineIntersectionPoint,
segmentIncludesPoint,
} from "../../math";
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
import { distanceToBindableElement } from "./distance";
import { intersectElementWithLine } from "./collision";
export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement>
| SuggestedPointBinding;
@ -556,12 +546,7 @@ const calculateFocusAndGap = (
elementsMap,
);
return {
focus: determineFocusDistance(
hoveredElement,
adjacentPoint,
edgePoint,
elementsMap,
),
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
};
};
@ -747,29 +732,26 @@ export const bindPointToSnapToElementOutline = (
p: Readonly<GlobalPoint>,
otherPoint: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement | undefined,
elementsMap: ElementsMap,
): GlobalPoint => {
const aabb = bindableElement && aabbForElement(bindableElement);
if (bindableElement && aabb) {
// TODO: Dirty hacks until tangents are properly calculated
const heading = headingForPointFromElement(bindableElement, aabb, p);
const intersections = [
const intersections: GlobalPoint[] = [
...(intersectElementWithLine(
bindableElement,
point(p[0], p[1] - 2 * bindableElement.height),
point(p[0], p[1] + 2 * bindableElement.height),
FIXED_BINDING_DISTANCE,
elementsMap,
) ?? []),
...(intersectElementWithLine(
bindableElement,
point(p[0] - 2 * bindableElement.width, p[1]),
point(p[0] + 2 * bindableElement.width, p[1]),
FIXED_BINDING_DISTANCE,
elementsMap,
) ?? []),
];
].filter((p) => p != null);
const isVertical =
compareHeading(heading, HEADING_LEFT) ||
@ -1043,7 +1025,6 @@ const updateBoundPoint = (
bindableElement,
binding.focus,
adjacentPoint,
elementsMap,
);
let newEdgePoint: GlobalPoint;
@ -1058,7 +1039,6 @@ const updateBoundPoint = (
adjacentPoint,
focusPointAbsolute,
binding.gap,
elementsMap,
);
if (!intersections || intersections.length === 0) {
// This should never happen, since focusPoint should always be
@ -1105,7 +1085,6 @@ export const calculateFixedPointForElbowArrowBinding = (
globalPoint,
otherGlobalPoint,
hoveredElement,
elementsMap,
);
const globalMidPoint = point(
bounds[0] + (bounds[2] - bounds[0]) / 2,
@ -1342,29 +1321,6 @@ export const maxBindingGap = (
return Math.max(16, Math.min(0.25 * smallerDimension, 32));
};
const relativizationToElementCenter = (
element: ExcalidrawElement,
elementsMap: ElementsMap,
): GA.Transform => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const center = coordsCenter(x1, y1, x2, y2);
// GA has angle orientation opposite to `rotate`
const rotate = GATransform.rotation(center, element.angle);
const translate = GA.reverse(
GATransform.translation(GADirection.from(center)),
);
return GATransform.compose(rotate, translate);
};
const coordsCenter = (
x1: number,
y1: number,
x2: number,
y2: number,
): GA.Point => {
return GA.point((x1 + x2) / 2, (y1 + y2) / 2);
};
// The focus distance is the oriented ratio between the size of
// the `element` and the "focus image" of the element on which
// all focus points lie, so it's a number between -1 and 1.
@ -1376,39 +1332,22 @@ const determineFocusDistance = (
a: GlobalPoint,
// Another point on the line, in absolute coordinates (closer to element)
b: GlobalPoint,
elementsMap: ElementsMap,
): number => {
const relateToCenter = relativizationToElementCenter(element, elementsMap);
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
const line = GALine.through(aRel, bRel);
const q = element.height / element.width;
const hwidth = element.width / 2;
const hheight = element.height / 2;
const n = line[2];
const m = line[3];
const c = line[1];
const mabs = Math.abs(m);
const nabs = Math.abs(n);
let ret;
switch (element.type) {
case "rectangle":
case "image":
case "text":
case "iframe":
case "embeddable":
case "frame":
case "magicframe":
ret = c / (hwidth * (nabs + q * mabs));
break;
case "diamond":
ret = mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
break;
case "ellipse":
ret = c / (hwidth * Math.sqrt(n ** 2 + q ** 2 * m ** 2));
break;
const center = point<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const p = pointRotateRads(b, center, radians(Math.PI / 2));
const intersection = lineLineIntersectionPoint(line(a, b), line(p, center));
if (!intersection) {
return 0;
}
return ret || 0;
return (
((segmentIncludesPoint(intersection, segment(center, p)) ? 1 : -1) *
pointDistance(center, intersection!)) /
pointDistance(center, b)
);
};
const determineFocusPoint = (
@ -1416,330 +1355,32 @@ const determineFocusPoint = (
// The oriented, relative distance from the center of `element` of the
// returned focusPoint
focus: number,
adjecentPoint: GlobalPoint,
elementsMap: ElementsMap,
adjacentPoint: GlobalPoint,
): GlobalPoint => {
if (focus === 0) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const center = coordsCenter(x1, y1, x2, y2);
return pointFromPair(GAPoint.toTuple(center));
}
const relateToCenter = relativizationToElementCenter(element, elementsMap);
const adjecentPointRel = GATransform.apply(
relateToCenter,
GAPoint.from(adjecentPoint),
);
const reverseRelateToCenter = GA.reverse(relateToCenter);
let p: GA.Point;
switch (element.type) {
case "rectangle":
case "image":
case "text":
case "diamond":
case "iframe":
case "embeddable":
case "frame":
case "magicframe":
p = findFocusPointForRectanguloidElement(
element,
focus,
adjecentPointRel,
);
break;
case "ellipse":
p = findFocusPointForEllipse(element, focus, adjecentPointRel);
break;
}
return pointFromPair(
GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, p)),
);
};
// Returns 2 or 0 intersection points between line going through `a` and `b`
// and the `element`, in ascending order of distance from `a`.
const intersectElementWithLine = (
element: ExcalidrawBindableElement,
// Point on the line, in absolute coordinates
a: GlobalPoint,
// Another point on the line, in absolute coordinates
b: GlobalPoint,
// If given, the element is inflated by this value
gap: number = 0,
elementsMap: ElementsMap,
): GlobalPoint[] | undefined => {
if (isRectangularElement(element)) {
return segmentIntersectRectangleElement(element, segment(a, b), gap);
}
const relateToCenter = relativizationToElementCenter(element, elementsMap);
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
const line = GALine.through(aRel, bRel);
const reverseRelateToCenter = GA.reverse(relateToCenter);
const intersections = getSortedElementLineIntersections(
element,
line,
aRel,
gap,
);
return intersections.map(
(point) =>
pointFromPair(
GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point)),
),
// pointFromArray(
// ,
// ),
);
};
const getSortedElementLineIntersections = (
element: ExcalidrawBindableElement,
// Relative to element center
line: GA.Line,
// Relative to element center
nearPoint: GA.Point,
gap: number = 0,
): GA.Point[] => {
let intersections: GA.Point[];
switch (element.type) {
case "rectangle":
case "image":
case "text":
case "diamond":
case "iframe":
case "embeddable":
case "frame":
case "magicframe":
const corners = getCorners(element);
intersections = corners
.flatMap((point, i) => {
const edge: [GA.Point, GA.Point] = [point, corners[(i + 1) % 4]];
return intersectSegment(line, offsetSegment(edge, gap));
})
.concat(
corners.flatMap((point) => getCircleIntersections(point, gap, line)),
);
break;
case "ellipse":
intersections = getEllipseIntersections(element, gap, line);
break;
}
if (intersections.length < 2) {
// Ignore the "edge" case of only intersecting with a single corner
return [];
}
const sortedIntersections = intersections.sort(
(i1, i2) =>
GAPoint.distance(i1, nearPoint) - GAPoint.distance(i2, nearPoint),
);
return [
sortedIntersections[0],
sortedIntersections[sortedIntersections.length - 1],
];
};
const getCorners = (
element:
| ExcalidrawRectangleElement
| ExcalidrawImageElement
| ExcalidrawDiamondElement
| ExcalidrawTextElement
| ExcalidrawIframeLikeElement
| ExcalidrawFrameLikeElement,
scale: number = 1,
): GA.Point[] => {
const hx = (scale * element.width) / 2;
const hy = (scale * element.height) / 2;
switch (element.type) {
case "rectangle":
case "image":
case "text":
case "iframe":
case "embeddable":
case "frame":
case "magicframe":
return [
GA.point(hx, hy),
GA.point(hx, -hy),
GA.point(-hx, -hy),
GA.point(-hx, hy),
];
case "diamond":
return [
GA.point(0, hy),
GA.point(hx, 0),
GA.point(0, -hy),
GA.point(-hx, 0),
];
}
};
// Returns intersection of `line` with `segment`, with `segment` moved by
// `gap` in its polar direction.
// If intersection coincides with second segment point returns empty array.
const intersectSegment = (
line: GA.Line,
segment: [GA.Point, GA.Point],
): GA.Point[] => {
const [a, b] = segment;
const aDist = GAPoint.distanceToLine(a, line);
const bDist = GAPoint.distanceToLine(b, line);
if (aDist * bDist >= 0) {
// The intersection is outside segment `(a, b)`
return [];
}
return [GAPoint.intersect(line, GALine.through(a, b))];
};
const offsetSegment = (
segment: [GA.Point, GA.Point],
distance: number,
): [GA.Point, GA.Point] => {
const [a, b] = segment;
const offset = GATransform.translationOrthogonal(
GADirection.fromTo(a, b),
distance,
);
return [GATransform.apply(offset, a), GATransform.apply(offset, b)];
};
const getEllipseIntersections = (
element: ExcalidrawEllipseElement,
gap: number,
line: GA.Line,
): GA.Point[] => {
const a = element.width / 2 + gap;
const b = element.height / 2 + gap;
const m = line[2];
const n = line[3];
const c = line[1];
const squares = a * a * m * m + b * b * n * n;
const discr = squares - c * c;
if (squares === 0 || discr <= 0) {
return [];
}
const discrRoot = Math.sqrt(discr);
const xn = -a * a * m * c;
const yn = -b * b * n * c;
return [
GA.point(
(xn + a * b * n * discrRoot) / squares,
(yn - a * b * m * discrRoot) / squares,
),
GA.point(
(xn - a * b * n * discrRoot) / squares,
(yn + a * b * m * discrRoot) / squares,
),
];
};
const getCircleIntersections = (
center: GA.Point,
radius: number,
line: GA.Line,
): GA.Point[] => {
if (radius === 0) {
return GAPoint.distanceToLine(line, center) === 0 ? [center] : [];
}
const m = line[2];
const n = line[3];
const c = line[1];
const [a, b] = GAPoint.toTuple(center);
const r = radius;
const squares = m * m + n * n;
const discr = r * r * squares - (m * a + n * b + c) ** 2;
if (squares === 0 || discr <= 0) {
return [];
}
const discrRoot = Math.sqrt(discr);
const xn = a * n * n - b * m * n - m * c;
const yn = b * m * m - a * m * n - n * c;
return [
GA.point((xn + n * discrRoot) / squares, (yn - m * discrRoot) / squares),
GA.point((xn - n * discrRoot) / squares, (yn + m * discrRoot) / squares),
];
};
// The focus point is the tangent point of the "focus image" of the
// `element`, where the tangent goes through `point`.
const findFocusPointForEllipse = (
ellipse: ExcalidrawEllipseElement,
// Between -1 and 1 (not 0) the relative size of the "focus image" of
// the element on which the focus point lies
relativeDistance: number,
// The point for which we're trying to find the focus point, relative
// to the ellipse center.
point: GA.Point,
): GA.Point => {
const relativeDistanceAbs = Math.abs(relativeDistance);
const a = (ellipse.width * relativeDistanceAbs) / 2;
const b = (ellipse.height * relativeDistanceAbs) / 2;
const orientation = Math.sign(relativeDistance);
const [px, pyo] = GAPoint.toTuple(point);
// The calculation below can't handle py = 0
const py = pyo === 0 ? 0.0001 : pyo;
const squares = px ** 2 * b ** 2 + py ** 2 * a ** 2;
// Tangent mx + ny + 1 = 0
const m =
(-px * b ** 2 +
orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
squares;
let n = (-m * px - 1) / py;
if (n === 0) {
// if zero {-0, 0}, fall back to a same-sign value in the similar range
n = (Object.is(n, -0) ? -1 : 1) * 0.01;
}
const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
return GA.point(x, (-m * x - 1) / n);
};
const findFocusPointForRectanguloidElement = (
element:
| ExcalidrawRectangleElement
| ExcalidrawImageElement
| ExcalidrawDiamondElement
| ExcalidrawTextElement
| ExcalidrawIframeLikeElement
| ExcalidrawFrameLikeElement,
// Between -1 and 1 for how far away should the focus point be relative
// to the size of the element. Sign determines orientation.
relativeDistance: number,
// The point for which we're trying to find the focus point, relative
// to the element center.
gaPoint: GA.Point,
): GA.Point => {
const relP = pointFromPair<GlobalPoint>(GAPoint.toTuple(gaPoint));
const center = point<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const p = point<GlobalPoint>(center[0] + relP[0], center[1] + relP[1]);
const ret = pointFromVector(
if (focus === 0) {
return center;
}
return pointFromVector(
vectorScale(
vectorRotate(
vectorNormalize(vectorFromPoint(p, center)),
vectorNormalize(vectorFromPoint(adjacentPoint, center)),
radians(Math.PI / 2),
),
Math.sign(relativeDistance) *
Math.sign(focus) *
Math.min(
pointDistance(point<GlobalPoint>(element.x, element.y), center) *
Math.abs(relativeDistance),
Math.abs(focus),
element.width / 2,
element.height / 2,
),
),
center,
);
return GA.point(ret[0] - center[0], ret[1] - center[1]);
};
export const bindingProperties: Set<BindableProp | BindingProp> = new Set([

@ -22,13 +22,18 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { ShapeCache } from "../scene/ShapeCache";
import { arrayToMap, invariant } from "../utils";
import type { GlobalPoint, LocalPoint } from "../../math";
import type { GlobalPoint, LocalPoint, Segment } from "../../math";
import {
point,
pointDistance,
pointFromArray,
pointRotateRads,
pointRescaleFromTopLeft,
segment,
ellipseSegmentInterceptPoints,
ellipse,
arc,
radians,
} from "../../math";
import type { Mutable } from "../utility-types";
import { getCurvePathOps } from "../../utils/geometry/shape";
@ -651,3 +656,55 @@ export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
bounds[0] + (bounds[2] - bounds[0]) / 2,
bounds[1] + (bounds[3] - bounds[1]) / 2,
);
/**
* Shortens a segment on both ends to accomodate the arc in the rounded
* diamond shape
*
* @param s The segment to shorten
* @param r The radius to shorten by
* @returns The segment shortened on both ends by the same radius
*/
export const createDiamondSide = (
s: Segment<GlobalPoint>,
startRadius: number,
endRadius: number,
): Segment<GlobalPoint> => {
return segment(
ellipseSegmentInterceptPoints(
ellipse(s[0], startRadius, startRadius),
s,
)[0] ?? s[0],
ellipseSegmentInterceptPoints(ellipse(s[1], endRadius, endRadius), s)[0] ??
s[1],
);
};
/**
* Creates an arc for the given roundness and position by taking the start
* and end positions and determining the angle points on the hypotethical
* circle with center point between start and end and raidus equals provided
* roundness. I.e. the created arc is gobal point-aware, or "rotated" in-place.
*
* @param start
* @param end
* @param r
* @returns
*/
export const createDiamondArc = (
start: GlobalPoint,
end: GlobalPoint,
r: number,
) => {
const c = point<GlobalPoint>(
(start[0] + end[0]) / 2,
(start[1] + end[1]) / 2,
);
return arc(
c,
r,
radians(Math.asin((start[1] - c[1]) / r)),
radians(Math.asin((end[1] - c[1]) / r)),
);
};

@ -1,9 +1,16 @@
import type {
ElementsMap,
ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawEllipseElement,
ExcalidrawRectangleElement,
ExcalidrawRectanguloidElement,
} from "./types";
import { getElementBounds } from "./bounds";
import {
createDiamondArc,
createDiamondSide,
getElementBounds,
} from "./bounds";
import type { FrameNameBounds } from "../types";
import type { GeometricShape } from "../../utils/geometry/shape";
import { getPolygonShape } from "../../utils/geometry/shape";
@ -15,9 +22,28 @@ import {
isImageElement,
isTextElement,
} from "./typeChecks";
import { getBoundTextShape } from "../shapes";
import type { GlobalPoint, Polygon } from "../../math";
import { pathIsALoop, isPointWithinBounds, point } from "../../math";
import {
getBoundTextShape,
getCornerRadius,
getDiamondPoints,
} from "../shapes";
import type { Arc, GlobalPoint, Polygon } from "../../math";
import {
pathIsALoop,
isPointWithinBounds,
point,
rectangle,
pointRotateRads,
radians,
segment,
arc,
lineSegmentIntersectionPoints,
line,
arcLineInterceptPoints,
pointDistanceSq,
ellipse,
ellipseLineIntersectionPoints,
} from "../../math";
import { LINE_CONFIRM_THRESHOLD } from "../constants";
export const shouldTestInside = (element: ExcalidrawElement) => {
@ -117,3 +143,226 @@ export const hitElementBoundText = (
): boolean => {
return !!textShape && isPointInShape(scenePointer, textShape);
};
export const intersectElementWithLine = (
element: ExcalidrawElement,
a: GlobalPoint,
b: GlobalPoint,
offset: number,
): GlobalPoint[] => {
switch (element.type) {
case "rectangle":
case "image":
case "text":
case "iframe":
case "embeddable":
case "frame":
case "magicframe":
return intersectRectanguloidWithLine(element, a, b, offset);
case "diamond":
return intersectDiamondWithLine(element, a, b, offset);
case "ellipse":
return intersectEllipseWithLine(element, a, b, offset);
default:
throw new Error(`Unimplemented element type '${element.type}'`);
}
};
export const intersectRectanguloidWithLine = (
element: ExcalidrawRectanguloidElement,
a: GlobalPoint,
b: GlobalPoint,
offset: number,
): GlobalPoint[] => {
const r = rectangle(
point(element.x - offset, element.y - offset),
point(
element.x + element.width + offset,
element.y + element.height + offset,
),
);
const center = point<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
// To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise.
const rotatedA = pointRotateRads<GlobalPoint>(
a,
center,
radians(-element.angle),
);
const rotatedB = pointRotateRads<GlobalPoint>(
b,
center,
radians(-element.angle),
);
const roundness = getCornerRadius(
Math.min(element.width + 2 * offset, element.height + 2 * offset),
element,
);
const sideIntersections: GlobalPoint[] = [
segment<GlobalPoint>(
point<GlobalPoint>(r[0][0] + roundness, r[0][1]),
point<GlobalPoint>(r[1][0] - roundness, r[0][1]),
),
segment<GlobalPoint>(
point<GlobalPoint>(r[1][0], r[0][1] + roundness),
point<GlobalPoint>(r[1][0], r[1][1] - roundness),
),
segment<GlobalPoint>(
point<GlobalPoint>(r[1][0] - roundness, r[1][1]),
point<GlobalPoint>(r[0][0] + roundness, r[1][1]),
),
segment<GlobalPoint>(
point<GlobalPoint>(r[0][0], r[1][1] - roundness),
point<GlobalPoint>(r[0][0], r[0][1] + roundness),
),
]
.map((s) =>
lineSegmentIntersectionPoints(line<GlobalPoint>(rotatedA, rotatedB), s),
)
.filter((x) => x != null)
.map((j) => pointRotateRads<GlobalPoint>(j, center, element.angle));
const cornerIntersections: GlobalPoint[] =
roundness > 0
? [
arc<GlobalPoint>(
point(r[0][0] + roundness, r[0][1] + roundness),
roundness,
radians(Math.PI),
radians((3 / 4) * Math.PI),
),
arc<GlobalPoint>(
point(r[1][0] - roundness, r[0][1] + roundness),
roundness,
radians((3 / 4) * Math.PI),
radians(0),
),
arc<GlobalPoint>(
point(r[1][0] - roundness, r[1][1] - roundness),
roundness,
radians(0),
radians((1 / 2) * Math.PI),
),
arc<GlobalPoint>(
point(r[0][0] + roundness, r[1][1] - roundness),
roundness,
radians((1 / 2) * Math.PI),
radians(Math.PI),
),
]
.flatMap((t) => arcLineInterceptPoints(t, line(rotatedA, rotatedB)))
.filter((i) => i != null)
.map((j) => pointRotateRads(j, center, element.angle))
: [];
return [...sideIntersections, ...cornerIntersections].sort(
(g, h) => pointDistanceSq(g!, b) - pointDistanceSq(h!, b),
);
};
/**
*
* @param element
* @param a
* @param b
* @returns
*/
export const intersectDiamondWithLine = (
element: ExcalidrawDiamondElement,
a: GlobalPoint,
b: GlobalPoint,
offset: number = 0,
): GlobalPoint[] => {
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
getDiamondPoints(element, offset);
const center = point<GlobalPoint>((topX + bottomX) / 2, (topY + bottomY) / 2);
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
const horizontalRadius = getCornerRadius(Math.abs(rightY - topY), element);
// Rotate the point to the inverse direction to simulate the rotated diamond
// points. It's all the same distance-wise.
const rotatedA = pointRotateRads(a, center, radians(-element.angle));
const rotatedB = pointRotateRads(b, center, radians(-element.angle));
const [top, right, bottom, left]: GlobalPoint[] = [
point(element.x + topX, element.y + topY),
point(element.x + rightX, element.y + rightY),
point(element.x + bottomX, element.y + bottomY),
point(element.x + leftX, element.y + leftY),
];
const topRight = createDiamondSide(
segment<GlobalPoint>(top, right),
verticalRadius,
horizontalRadius,
);
const bottomRight = createDiamondSide(
segment<GlobalPoint>(bottom, right),
verticalRadius,
horizontalRadius,
);
const bottomLeft = createDiamondSide(
segment<GlobalPoint>(bottom, left),
verticalRadius,
horizontalRadius,
);
const topLeft = createDiamondSide(
segment<GlobalPoint>(top, left),
verticalRadius,
horizontalRadius,
);
const arcs: Arc<GlobalPoint>[] = element.roundness
? [
createDiamondArc(topLeft[0], topRight[0], verticalRadius), // TOP
createDiamondArc(topRight[1], bottomRight[1], horizontalRadius), // RIGHT
createDiamondArc(bottomRight[0], bottomLeft[0], verticalRadius), // BOTTOM
createDiamondArc(bottomLeft[1], topLeft[1], horizontalRadius), // LEFT
]
: [];
const sides: GlobalPoint[] = [topRight, bottomRight, bottomLeft, topLeft]
.map((s) =>
lineSegmentIntersectionPoints(line<GlobalPoint>(rotatedA, rotatedB), s),
)
.filter((x) => x != null)
// Rotate back intersection points
.map((p) => pointRotateRads<GlobalPoint>(p, center, element.angle));
const corners = arcs
.flatMap((x) => arcLineInterceptPoints(x, line(rotatedA, rotatedB)))
.filter((x) => x != null)
// Rotate back intersection points
.map((p) => pointRotateRads(p, center, element.angle));
return [...sides, ...corners].sort(
(g, h) => pointDistanceSq(g!, b) - pointDistanceSq(h!, b),
);
};
/**
*
* @param element
* @param a
* @param b
* @returns
*/
export const intersectEllipseWithLine = (
element: ExcalidrawEllipseElement,
a: GlobalPoint,
b: GlobalPoint,
offset: number = 0,
): GlobalPoint[] => {
const center = point<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const rotatedA = pointRotateRads(a, center, radians(-element.angle));
const rotatedB = pointRotateRads(b, center, radians(-element.angle));
return ellipseLineIntersectionPoints(
ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
line(rotatedA, rotatedB),
).map((p) => pointRotateRads(p, center, element.angle));
};

@ -1,10 +1,9 @@
import type { GlobalPoint, Segment } from "../../math";
import type { GlobalPoint } from "../../math";
import {
arc,
arcDistanceFromPoint,
ellipse,
ellipseDistanceFromPoint,
ellipseSegmentInterceptPoints,
point,
pointRotateRads,
radians,
@ -13,6 +12,7 @@ import {
segmentDistanceToPoint,
} from "../../math";
import { getCornerRadius, getDiamondPoints } from "../shapes";
import { createDiamondArc, createDiamondSide } from "./bounds";
import type {
ExcalidrawBindableElement,
ExcalidrawDiamondElement,
@ -22,7 +22,7 @@ import type {
export const distanceToBindableElement = (
element: ExcalidrawBindableElement,
point: GlobalPoint,
p: GlobalPoint,
): number => {
switch (element.type) {
case "rectangle":
@ -32,11 +32,11 @@ export const distanceToBindableElement = (
case "embeddable":
case "frame":
case "magicframe":
return distanceToRectangleElement(element, point);
return distanceToRectangleElement(element, p);
case "diamond":
return distanceToDiamondElement(element, point);
return distanceToDiamondElement(element, p);
case "ellipse":
return distanceToEllipseElement(element, point);
return distanceToEllipseElement(element, p);
}
};
@ -118,54 +118,6 @@ export const distanceToRectangleElement = (
return Math.min(...[...sideDistances, ...cornerDistances]);
};
/**
* Shortens a segment on both ends to accomodate the arc in the rounded
* diamond shape
*
* @param s The segment to shorten
* @param r The radius to shorten by
* @returns The segment shortened on both ends by the same radius
*/
const createDiamondSide = (
s: Segment<GlobalPoint>,
startRadius: number,
endRadius: number,
): Segment<GlobalPoint> => {
return segment(
ellipseSegmentInterceptPoints(
ellipse(s[0], startRadius, startRadius),
s,
)[0] ?? s[0],
ellipseSegmentInterceptPoints(ellipse(s[1], endRadius, endRadius), s)[0] ??
s[1],
);
};
/**
* Creates an arc for the given roundness and position by taking the start
* and end positions and determining the angle points on the hypotethical
* circle with center point between start and end and raidus equals provided
* roundness. I.e. the created arc is gobal point-aware, or "rotated" in-place.
*
* @param start
* @param end
* @param r
* @returns
*/
const createDiamondArc = (start: GlobalPoint, end: GlobalPoint, r: number) => {
const c = point<GlobalPoint>(
(start[0] + end[0]) / 2,
(start[1] + end[1]) / 2,
);
return arc(
c,
r,
radians(Math.asin((start[1] - c[1]) / r)),
radians(Math.asin((end[1] - c[1]) / r)),
);
};
/**
* Returns the distance of a point and the provided diamond element, accounting
* for roundness and rotation

@ -1043,7 +1043,6 @@ const getSnapPoint = (
isRectanguloidElement(element) ? avoidRectangularCorner(element, p) : p,
otherPoint,
element,
elementsMap,
);
const getBindPointHeading = (

@ -474,7 +474,10 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
return 0;
};
export const getDiamondPoints = (element: ExcalidrawDiamondElement) => {
export const getDiamondPoints = (
element: ExcalidrawDiamondElement,
offset: number = 0,
) => {
// Here we add +1 to avoid these numbers to be 0
// otherwise rough.js will throw an error complaining about it
const topX = Math.floor(element.width / 2) + 1;
@ -486,5 +489,14 @@ export const getDiamondPoints = (element: ExcalidrawDiamondElement) => {
const leftX = 0;
const leftY = rightY;
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
return [
topX - offset,
topY - offset,
rightX + offset,
rightY + offset,
bottomX + offset,
bottomY + offset,
leftX - offset,
leftY - offset,
];
};

@ -194,7 +194,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 99,
"height": "121.17708",
"id": "id166",
"index": "a2",
"isDeleted": false,
@ -208,8 +208,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
"98.20800",
99,
"120.20767",
"121.17708",
],
],
"roughness": 1,
@ -224,7 +224,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 40,
"width": "98.20800",
"width": "120.20767",
"x": 1,
"y": 0,
}
@ -292,24 +292,24 @@ History {
"endBinding": {
"elementId": "id165",
"fixedPoint": null,
"focus": "0.00990",
"focus": "0.01000",
"gap": 1,
},
"height": "1.37272",
"height": "1.78061",
"points": [
[
0,
0,
],
[
98,
"-1.37272",
"128.08994",
"-1.78061",
],
],
"startBinding": {
"elementId": "id164",
"fixedPoint": null,
"focus": "0.02970",
"focus": "0.03001",
"gap": 1,
},
},
@ -320,15 +320,15 @@ History {
"focus": "-0.02000",
"gap": 1,
},
"height": "0.00473",
"height": "0.00968",
"points": [
[
0,
0,
],
[
"98.00000",
"0.00473",
302,
"-0.00968",
],
],
"startBinding": {
@ -390,15 +390,15 @@ History {
"focus": 0,
"gap": 1,
},
"height": 99,
"height": "121.17708",
"points": [
[
0,
0,
],
[
"98.20800",
99,
"120.20767",
"121.17708",
],
],
"startBinding": null,
@ -408,27 +408,27 @@ History {
"endBinding": {
"elementId": "id165",
"fixedPoint": null,
"focus": "0.00990",
"focus": "0.01000",
"gap": 1,
},
"height": "1.37680",
"height": "1.02669",
"points": [
[
0,
0,
],
[
"98.00000",
"-1.37680",
"128.74676",
"-1.02669",
],
],
"startBinding": {
"elementId": "id164",
"fixedPoint": null,
"focus": "0.02970",
"focus": "0.03001",
"gap": 1,
},
"y": "1.39313",
"y": "0.48108",
},
},
"id169" => Delta {
@ -819,7 +819,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"updated": 1,
"version": 30,
"width": 0,
"x": 200,
"x": "174.50000",
"y": 0,
}
`;
@ -1237,7 +1237,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "2.61991",
"height": "0.13739",
"id": "id172",
"index": "Zz",
"isDeleted": false,
@ -1251,8 +1251,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
"98.00000",
"-2.61991",
"124.66911",
"0.13739",
],
],
"roughness": 1,
@ -1275,9 +1275,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 11,
"width": "98.00000",
"x": "1.00000",
"y": "3.98333",
"width": "124.66911",
"x": 1,
"y": "-0.16421",
}
`;
@ -1605,7 +1605,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "2.61991",
"height": "0.13739",
"id": "id175",
"index": "a0",
"isDeleted": false,
@ -1619,8 +1619,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
"98.00000",
"-2.61991",
"124.66911",
"0.13739",
],
],
"roughness": 1,
@ -1643,9 +1643,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 11,
"width": "98.00000",
"x": "1.00000",
"y": "3.98333",
"width": "124.66911",
"x": 1,
"y": "-0.16421",
}
`;
@ -1763,7 +1763,7 @@ History {
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "22.36242",
"height": "5.44620",
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
@ -1776,8 +1776,8 @@ History {
0,
],
[
"98.00000",
"-22.36242",
"188.94246",
"5.44620",
],
],
"roughness": 1,
@ -1798,9 +1798,9 @@ History {
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"width": "98.00000",
"x": 1,
"y": 34,
"width": "188.94246",
"x": "-59.03817",
"y": "-6.02545",
},
"inserted": {
"isDeleted": true,
@ -2311,7 +2311,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "408.19672",
"height": "414.71403",
"id": "id180",
"index": "a2",
"isDeleted": false,
@ -2325,8 +2325,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
498,
"-408.19672",
"576.45250",
"-414.71403",
],
],
"roughness": 1,
@ -2346,8 +2346,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 10,
"width": 498,
"x": 1,
"width": "576.45250",
"x": "-75.50000",
"y": 0,
}
`;
@ -14997,7 +14997,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0,
],
[
"98.00000",
200,
0,
],
],
@ -15018,8 +15018,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
"updated": 1,
"version": 10,
"width": "98.00000",
"x": 1,
"width": 200,
"x": "-75.50000",
"y": 0,
}
`;
@ -15693,7 +15693,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0,
],
[
"98.00000",
200,
0,
],
],
@ -15714,8 +15714,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
"updated": 1,
"version": 10,
"width": "98.00000",
"x": 1,
"width": 200,
"x": "-75.50000",
"y": 0,
}
`;
@ -16313,7 +16313,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0,
],
[
"98.00000",
200,
0,
],
],
@ -16334,8 +16334,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
"updated": 1,
"version": 10,
"width": "98.00000",
"x": 1,
"width": 200,
"x": "-75.50000",
"y": 0,
}
`;
@ -16931,7 +16931,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0,
],
[
"98.00000",
200,
0,
],
],
@ -16952,8 +16952,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
"updated": 1,
"version": 10,
"width": "98.00000",
"x": 1,
"width": 200,
"x": "-75.50000",
"y": 0,
}
`;
@ -17646,7 +17646,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0,
],
[
"98.00000",
200,
0,
],
],
@ -17667,8 +17667,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow",
"updated": 1,
"version": 11,
"width": "98.00000",
"x": 1,
"width": 200,
"x": "-75.50000",
"y": 0,
}
`;

@ -49,9 +49,3 @@ exports[`Test Linear Elements > Test bound text element > should wrap the bound
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = `
"Online whiteboard
collaboration made
easy"
`;

@ -101,141 +101,3 @@ exports[`move element > rectangle 5`] = `
"y": 40,
}
`;
exports[`move element > rectangles with binding arrow 5`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id2",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id0",
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 3,
},
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1723083209,
"width": 100,
"x": 0,
"y": 0,
}
`;
exports[`move element > rectangles with binding arrow 6`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id2",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 300,
"id": "id1",
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 3,
},
"seed": 1150084233,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"versionNonce": 745419401,
"width": 300,
"x": 201,
"y": 2,
}
`;
exports[`move element > rectangles with binding arrow 7`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id1",
"fixedPoint": null,
"focus": "-0.46667",
"gap": 10,
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "77.29870",
"id": "id2",
"index": "a2",
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
81,
"77.29870",
],
],
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 1604849351,
"startArrowhead": null,
"startBinding": {
"elementId": "id0",
"fixedPoint": null,
"focus": "-0.60000",
"gap": 9,
},
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 11,
"versionNonce": 1996028265,
"width": 81,
"x": 110,
"y": 50,
}
`;

@ -1,5 +1,11 @@
import { radians } from "./angle";
import { arc, arcIncludesPoint, arcSegmentInterceptPoints } from "./arc";
import {
arc,
arcIncludesPoint,
arcLineInterceptPoints,
arcSegmentInterceptPoints,
} from "./arc";
import { line } from "./line";
import { point } from "./point";
import { segment } from "./segment";
@ -31,7 +37,7 @@ describe("point on arc", () => {
});
describe("intersection", () => {
it("should report correct interception point", () => {
it("should report correct interception point for segment", () => {
expect(
arcSegmentInterceptPoints(
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
@ -40,7 +46,7 @@ describe("intersection", () => {
).toEqual([point(0.894427190999916, 0.447213595499958)]);
});
it("should report both interception points when present", () => {
it("should report both interception points when present for segment", () => {
expect(
arcSegmentInterceptPoints(
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
@ -51,4 +57,25 @@ describe("intersection", () => {
point(0.9, 0.4358898943540668),
]);
});
it("should report correct interception point for line", () => {
expect(
arcLineInterceptPoints(
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
line(point(2, 1), point(0, 0)),
),
).toEqual([point(0.894427190999916, 0.447213595499958)]);
});
it("should report both interception points when present for line", () => {
expect(
arcLineInterceptPoints(
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
line(point(0.9, -2), point(0.9, 2)),
),
).toEqual([
point(0.9, 0.4358898943540668),
point(0.9, -0.4358898943540668),
]);
});
});

@ -2,10 +2,11 @@ import { cartesian2Polar, normalizeRadians, radians } from "./angle";
import {
ellipse,
ellipseDistanceFromPoint,
ellipseLineIntersectionPoints,
ellipseSegmentInterceptPoints,
} from "./ellipse";
import { point, pointDistance } from "./point";
import type { GenericPoint, Segment, Radians, Arc } from "./types";
import type { GenericPoint, Segment, Radians, Arc, Line } from "./types";
import { PRECISION } from "./utils";
/**
@ -85,7 +86,7 @@ export function arcDistanceFromPoint<Point extends GenericPoint>(
/**
* Returns the intersection point(s) of a line segment represented by a start
* point and end point and a symmetric arc.
* point and end point and a symmetric arc
*/
export function arcSegmentInterceptPoints<Point extends GenericPoint>(
a: Readonly<Arc<Point>>,
@ -106,3 +107,31 @@ export function arcSegmentInterceptPoints<Point extends GenericPoint>(
: a.startAngle <= candidateAngle || a.endAngle >= candidateAngle;
});
}
/**
* Returns the intersection point(s) of a line segment represented by a start
* point and end point and a symmetric arc
*
* @param a
* @param l
* @returns
*/
export function arcLineInterceptPoints<Point extends GenericPoint>(
a: Readonly<Arc<Point>>,
l: Readonly<Line<Point>>,
): Point[] {
return ellipseLineIntersectionPoints(
ellipse(a.center, a.radius, a.radius),
l,
).filter((candidate) => {
const [candidateRadius, candidateAngle] = cartesian2Polar(
point(candidate[0] - a.center[0], candidate[1] - a.center[1]),
);
return a.startAngle < a.endAngle
? Math.abs(a.radius - candidateRadius) < PRECISION &&
a.startAngle <= candidateAngle &&
a.endAngle >= candidateAngle
: a.startAngle <= candidateAngle || a.endAngle >= candidateAngle;
});
}

@ -3,7 +3,7 @@ import {
ellipseSegmentInterceptPoints,
ellipseIncludesPoint,
ellipseTouchesPoint,
ellipseIntersectsLine,
ellipseLineIntersectionPoints,
} from "./ellipse";
import { line } from "./line";
import { point } from "./point";
@ -85,7 +85,7 @@ describe("line and ellipse", () => {
it("detects outside line", () => {
expect(
ellipseIntersectsLine(
ellipseLineIntersectionPoints(
e,
line<GlobalPoint>(point(-10, -10), point(10, -10)),
),
@ -93,10 +93,10 @@ describe("line and ellipse", () => {
});
it("detects line intersecting ellipse", () => {
expect(
ellipseIntersectsLine(e, line<GlobalPoint>(point(0, -1), point(0, 1))),
ellipseLineIntersectionPoints(e, line<GlobalPoint>(point(0, -1), point(0, 1))),
).toEqual([point(0, 2), point(0, -2)]);
expect(
ellipseIntersectsLine(
ellipseLineIntersectionPoints(
e,
line<GlobalPoint>(point(-100, 0), point(-10, 0)),
).map(([x, y]) => point(Math.round(x), Math.round(y))),
@ -104,7 +104,7 @@ describe("line and ellipse", () => {
});
it("detects line touching ellipse", () => {
expect(
ellipseIntersectsLine(e, line<GlobalPoint>(point(-2, -2), point(2, -2))),
ellipseLineIntersectionPoints(e, line<GlobalPoint>(point(-2, -2), point(2, -2))),
).toEqual([point(0, -2)]);
});
});

@ -181,7 +181,7 @@ export function ellipseSegmentInterceptPoints<Point extends GenericPoint>(
return intersections;
}
export function ellipseIntersectsLine<Point extends GenericPoint>(
export function ellipseLineIntersectionPoints<Point extends GenericPoint>(
{ center, halfWidth, halfHeight }: Ellipse<Point>,
[g, h]: Line<Point>,
): Point[] {

@ -1,11 +1,11 @@
import { line, lineIntersectsLine, lineIntersectsSegment } from "./line";
import { line, lineLineIntersectionPoint, lineSegmentIntersectionPoints } from "./line";
import { point } from "./point";
import { segment } from "./segment";
describe("line-line intersections", () => {
it("should correctly detect intersection at origin", () => {
expect(
lineIntersectsLine(
lineLineIntersectionPoint(
line(point(-5, -5), point(5, 5)),
line(point(5, -5), point(-5, 5)),
),
@ -14,7 +14,7 @@ describe("line-line intersections", () => {
it("should correctly detect intersection at non-origin", () => {
expect(
lineIntersectsLine(
lineLineIntersectionPoint(
line(point(0, 0), point(10, 10)),
line(point(10, 0), point(0, 10)),
),
@ -23,7 +23,7 @@ describe("line-line intersections", () => {
it("should correctly detect parallel lines", () => {
expect(
lineIntersectsLine(
lineLineIntersectionPoint(
line(point(0, 0), point(0, 10)),
line(point(10, 0), point(10, 10)),
),
@ -34,7 +34,7 @@ describe("line-line intersections", () => {
describe("line-segment intersections", () => {
it("should correctly detect intersection", () => {
expect(
lineIntersectsSegment(
lineSegmentIntersectionPoints(
line(point(0, 0), point(5, 0)),
segment(point(2, -2), point(3, 2)),
),
@ -42,7 +42,7 @@ describe("line-segment intersections", () => {
});
it("should correctly detect non-intersection", () => {
expect(
lineIntersectsSegment(
lineSegmentIntersectionPoints(
line(point(0, 0), point(5, 0)),
segment(point(3, 1), point(4, 4)),
),

@ -1,4 +1,4 @@
import { ellipseIntersectsLine } from "./ellipse";
import { ellipseLineIntersectionPoints } from "./ellipse";
import { point, pointCenter, pointRotateRads } from "./point";
import { segmentIncludesPoint } from "./segment";
import type { GenericPoint, Line, Radians, Segment } from "./types";
@ -60,7 +60,7 @@ export function lineRotate<Point extends GenericPoint>(
* @param b Another line to intersect
* @returns The intersection point
*/
export function lineIntersectsLine<Point extends GenericPoint>(
export function lineLineIntersectionPoint<Point extends GenericPoint>(
[[x1, y1], [x2, y2]]: Line<Point>,
[[x3, y3], [x4, y4]]: Line<Point>,
): Point | null {
@ -83,11 +83,11 @@ export function lineIntersectsLine<Point extends GenericPoint>(
* @param s
* @returns
*/
export function lineIntersectsSegment<Point extends GenericPoint>(
export function lineSegmentIntersectionPoints<Point extends GenericPoint>(
l: Line<Point>,
s: Segment<Point>,
): Point | null {
const candidate = lineIntersectsLine(l, line(s[0], s[1]));
const candidate = lineLineIntersectionPoint(l, line(s[0], s[1]));
if (!candidate || !segmentIncludesPoint(candidate, s)) {
return null;
}
@ -95,4 +95,4 @@ export function lineIntersectsSegment<Point extends GenericPoint>(
return candidate;
}
export const lineInterceptsEllipse = ellipseIntersectsLine;
export const lineInterceptsEllipse = ellipseLineIntersectionPoints;

@ -1,4 +1,4 @@
import { lineIntersectsSegment } from "./line";
import { lineSegmentIntersectionPoints } from "./line";
import {
isPoint,
pointCenter,
@ -186,4 +186,4 @@ export function segmentDistanceToPoint<Point extends GenericPoint>(
* @param s
* @returns
*/
export const segmentIntersectsLine = lineIntersectsSegment;
export const segmentLineIntersectionPoints = lineSegmentIntersectionPoints;

Loading…
Cancel
Save