mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Replace intersection code
This commit is contained in:
parent
411deae176
commit
0c02972695
18 changed files with 507 additions and 687 deletions
|
@ -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…
Add table
Add a link
Reference in a new issue