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,
|
startGlobalPoint,
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
startHoveredElement,
|
startHoveredElement,
|
||||||
elementsMap,
|
|
||||||
)
|
)
|
||||||
: startGlobalPoint;
|
: startGlobalPoint;
|
||||||
const finalEndPoint = endHoveredElement
|
const finalEndPoint = endHoveredElement
|
||||||
|
@ -1622,7 +1621,6 @@ export const actionChangeArrowType = register({
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
endHoveredElement,
|
endHoveredElement,
|
||||||
elementsMap,
|
|
||||||
)
|
)
|
||||||
: endGlobalPoint;
|
: endGlobalPoint;
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": -0.008153707962747813,
|
"focus": -0.008835048729392623,
|
||||||
"gap": 11.562288374879595,
|
"gap": 11.562288374879595,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
@ -120,7 +120,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id49",
|
"elementId": "id49",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": -0.08139534883720931,
|
"focus": -0.08860759493670874,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1864ab",
|
"strokeColor": "#1864ab",
|
||||||
|
@ -147,7 +147,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0.10666666666666667,
|
"focus": 0.1045751633986928,
|
||||||
"gap": 3.8343264684446097,
|
"gap": 3.8343264684446097,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
@ -1485,7 +1485,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "Alice",
|
"elementId": "Alice",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 1.7573472843231123e-16,
|
||||||
"gap": 5.299874999999986,
|
"gap": 5.299874999999986,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"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 {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawRectangleElement,
|
|
||||||
ExcalidrawDiamondElement,
|
|
||||||
ExcalidrawEllipseElement,
|
|
||||||
ExcalidrawImageElement,
|
|
||||||
ExcalidrawFrameLikeElement,
|
|
||||||
ExcalidrawIframeLikeElement,
|
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
PointBinding,
|
PointBinding,
|
||||||
|
@ -28,7 +16,7 @@ import type {
|
||||||
Bounds,
|
Bounds,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getCenterForBounds, getElementAbsoluteCoords } from "./bounds";
|
import { getCenterForBounds } from "./bounds";
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { isPointOnShape } from "../../utils/collision";
|
import { isPointOnShape } from "../../utils/collision";
|
||||||
import { getElementAtPosition } from "../scene";
|
import { getElementAtPosition } from "../scene";
|
||||||
|
@ -41,7 +29,6 @@ import {
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isRectangularElement,
|
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
|
@ -69,7 +56,6 @@ import {
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
pointFromPair,
|
|
||||||
pointDistanceSq,
|
pointDistanceSq,
|
||||||
clamp,
|
clamp,
|
||||||
radians,
|
radians,
|
||||||
|
@ -78,10 +64,14 @@ import {
|
||||||
vectorRotate,
|
vectorRotate,
|
||||||
vectorNormalize,
|
vectorNormalize,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
|
line,
|
||||||
|
lineLineIntersectionPoint,
|
||||||
|
segmentIncludesPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToBindableElement } from "./distance";
|
||||||
|
|
||||||
|
import { intersectElementWithLine } from "./collision";
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
| NonDeleted<ExcalidrawBindableElement>
|
| NonDeleted<ExcalidrawBindableElement>
|
||||||
| SuggestedPointBinding;
|
| SuggestedPointBinding;
|
||||||
|
@ -556,12 +546,7 @@ const calculateFocusAndGap = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
focus: determineFocusDistance(
|
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
||||||
hoveredElement,
|
|
||||||
adjacentPoint,
|
|
||||||
edgePoint,
|
|
||||||
elementsMap,
|
|
||||||
),
|
|
||||||
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -747,29 +732,26 @@ export const bindPointToSnapToElementOutline = (
|
||||||
p: Readonly<GlobalPoint>,
|
p: Readonly<GlobalPoint>,
|
||||||
otherPoint: Readonly<GlobalPoint>,
|
otherPoint: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement | undefined,
|
bindableElement: ExcalidrawBindableElement | undefined,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const aabb = bindableElement && aabbForElement(bindableElement);
|
const aabb = bindableElement && aabbForElement(bindableElement);
|
||||||
|
|
||||||
if (bindableElement && aabb) {
|
if (bindableElement && aabb) {
|
||||||
// TODO: Dirty hacks until tangents are properly calculated
|
// TODO: Dirty hacks until tangents are properly calculated
|
||||||
const heading = headingForPointFromElement(bindableElement, aabb, p);
|
const heading = headingForPointFromElement(bindableElement, aabb, p);
|
||||||
const intersections = [
|
const intersections: GlobalPoint[] = [
|
||||||
...(intersectElementWithLine(
|
...(intersectElementWithLine(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
point(p[0], p[1] - 2 * bindableElement.height),
|
point(p[0], p[1] - 2 * bindableElement.height),
|
||||||
point(p[0], p[1] + 2 * bindableElement.height),
|
point(p[0], p[1] + 2 * bindableElement.height),
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
elementsMap,
|
|
||||||
) ?? []),
|
) ?? []),
|
||||||
...(intersectElementWithLine(
|
...(intersectElementWithLine(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
point(p[0] - 2 * bindableElement.width, p[1]),
|
point(p[0] - 2 * bindableElement.width, p[1]),
|
||||||
point(p[0] + 2 * bindableElement.width, p[1]),
|
point(p[0] + 2 * bindableElement.width, p[1]),
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
elementsMap,
|
|
||||||
) ?? []),
|
) ?? []),
|
||||||
];
|
].filter((p) => p != null);
|
||||||
|
|
||||||
const isVertical =
|
const isVertical =
|
||||||
compareHeading(heading, HEADING_LEFT) ||
|
compareHeading(heading, HEADING_LEFT) ||
|
||||||
|
@ -1043,7 +1025,6 @@ const updateBoundPoint = (
|
||||||
bindableElement,
|
bindableElement,
|
||||||
binding.focus,
|
binding.focus,
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
elementsMap,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let newEdgePoint: GlobalPoint;
|
let newEdgePoint: GlobalPoint;
|
||||||
|
@ -1058,7 +1039,6 @@ const updateBoundPoint = (
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
focusPointAbsolute,
|
focusPointAbsolute,
|
||||||
binding.gap,
|
binding.gap,
|
||||||
elementsMap,
|
|
||||||
);
|
);
|
||||||
if (!intersections || intersections.length === 0) {
|
if (!intersections || intersections.length === 0) {
|
||||||
// This should never happen, since focusPoint should always be
|
// This should never happen, since focusPoint should always be
|
||||||
|
@ -1105,7 +1085,6 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||||
globalPoint,
|
globalPoint,
|
||||||
otherGlobalPoint,
|
otherGlobalPoint,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
elementsMap,
|
|
||||||
);
|
);
|
||||||
const globalMidPoint = point(
|
const globalMidPoint = point(
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
|
@ -1342,29 +1321,6 @@ export const maxBindingGap = (
|
||||||
return Math.max(16, Math.min(0.25 * smallerDimension, 32));
|
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 focus distance is the oriented ratio between the size of
|
||||||
// the `element` and the "focus image" of the element on which
|
// the `element` and the "focus image" of the element on which
|
||||||
// all focus points lie, so it's a number between -1 and 1.
|
// all focus points lie, so it's a number between -1 and 1.
|
||||||
|
@ -1376,39 +1332,22 @@ const determineFocusDistance = (
|
||||||
a: GlobalPoint,
|
a: GlobalPoint,
|
||||||
// Another point on the line, in absolute coordinates (closer to element)
|
// Another point on the line, in absolute coordinates (closer to element)
|
||||||
b: GlobalPoint,
|
b: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): number => {
|
): number => {
|
||||||
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
const center = point<GlobalPoint>(
|
||||||
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
|
element.x + element.width / 2,
|
||||||
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
|
element.y + element.height / 2,
|
||||||
const line = GALine.through(aRel, bRel);
|
);
|
||||||
const q = element.height / element.width;
|
const p = pointRotateRads(b, center, radians(Math.PI / 2));
|
||||||
const hwidth = element.width / 2;
|
const intersection = lineLineIntersectionPoint(line(a, b), line(p, center));
|
||||||
const hheight = element.height / 2;
|
if (!intersection) {
|
||||||
const n = line[2];
|
return 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return ret || 0;
|
|
||||||
|
return (
|
||||||
|
((segmentIncludesPoint(intersection, segment(center, p)) ? 1 : -1) *
|
||||||
|
pointDistance(center, intersection!)) /
|
||||||
|
pointDistance(center, b)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const determineFocusPoint = (
|
const determineFocusPoint = (
|
||||||
|
@ -1416,330 +1355,32 @@ const determineFocusPoint = (
|
||||||
// The oriented, relative distance from the center of `element` of the
|
// The oriented, relative distance from the center of `element` of the
|
||||||
// returned focusPoint
|
// returned focusPoint
|
||||||
focus: number,
|
focus: number,
|
||||||
adjecentPoint: GlobalPoint,
|
adjacentPoint: GlobalPoint,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): 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>(
|
const center = point<GlobalPoint>(
|
||||||
element.x + element.width / 2,
|
element.x + element.width / 2,
|
||||||
element.y + element.height / 2,
|
element.y + element.height / 2,
|
||||||
);
|
);
|
||||||
const p = point<GlobalPoint>(center[0] + relP[0], center[1] + relP[1]);
|
if (focus === 0) {
|
||||||
const ret = pointFromVector(
|
return center;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pointFromVector(
|
||||||
vectorScale(
|
vectorScale(
|
||||||
vectorRotate(
|
vectorRotate(
|
||||||
vectorNormalize(vectorFromPoint(p, center)),
|
vectorNormalize(vectorFromPoint(adjacentPoint, center)),
|
||||||
radians(Math.PI / 2),
|
radians(Math.PI / 2),
|
||||||
),
|
),
|
||||||
Math.sign(relativeDistance) *
|
Math.sign(focus) *
|
||||||
Math.min(
|
Math.min(
|
||||||
pointDistance(point<GlobalPoint>(element.x, element.y), center) *
|
pointDistance(point<GlobalPoint>(element.x, element.y), center) *
|
||||||
Math.abs(relativeDistance),
|
Math.abs(focus),
|
||||||
element.width / 2,
|
element.width / 2,
|
||||||
element.height / 2,
|
element.height / 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
center,
|
center,
|
||||||
);
|
);
|
||||||
|
|
||||||
return GA.point(ret[0] - center[0], ret[1] - center[1]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bindingProperties: Set<BindableProp | BindingProp> = new Set([
|
export const bindingProperties: Set<BindableProp | BindingProp> = new Set([
|
||||||
|
|
|
@ -22,13 +22,18 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { arrayToMap, invariant } from "../utils";
|
import { arrayToMap, invariant } from "../utils";
|
||||||
import type { GlobalPoint, LocalPoint } from "../../math";
|
import type { GlobalPoint, LocalPoint, Segment } from "../../math";
|
||||||
import {
|
import {
|
||||||
point,
|
point,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointRescaleFromTopLeft,
|
pointRescaleFromTopLeft,
|
||||||
|
segment,
|
||||||
|
ellipseSegmentInterceptPoints,
|
||||||
|
ellipse,
|
||||||
|
arc,
|
||||||
|
radians,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { getCurvePathOps } from "../../utils/geometry/shape";
|
import { getCurvePathOps } from "../../utils/geometry/shape";
|
||||||
|
@ -651,3 +656,55 @@ export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
bounds[1] + (bounds[3] - bounds[1]) / 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 {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getElementBounds } from "./bounds";
|
import {
|
||||||
|
createDiamondArc,
|
||||||
|
createDiamondSide,
|
||||||
|
getElementBounds,
|
||||||
|
} from "./bounds";
|
||||||
import type { FrameNameBounds } from "../types";
|
import type { FrameNameBounds } from "../types";
|
||||||
import type { GeometricShape } from "../../utils/geometry/shape";
|
import type { GeometricShape } from "../../utils/geometry/shape";
|
||||||
import { getPolygonShape } from "../../utils/geometry/shape";
|
import { getPolygonShape } from "../../utils/geometry/shape";
|
||||||
|
@ -15,9 +22,28 @@ import {
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { getBoundTextShape } from "../shapes";
|
import {
|
||||||
import type { GlobalPoint, Polygon } from "../../math";
|
getBoundTextShape,
|
||||||
import { pathIsALoop, isPointWithinBounds, point } from "../../math";
|
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";
|
import { LINE_CONFIRM_THRESHOLD } from "../constants";
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
|
@ -117,3 +143,226 @@ export const hitElementBoundText = (
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return !!textShape && isPointInShape(scenePointer, textShape);
|
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 {
|
import {
|
||||||
arc,
|
arc,
|
||||||
arcDistanceFromPoint,
|
arcDistanceFromPoint,
|
||||||
ellipse,
|
ellipse,
|
||||||
ellipseDistanceFromPoint,
|
ellipseDistanceFromPoint,
|
||||||
ellipseSegmentInterceptPoints,
|
|
||||||
point,
|
point,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
radians,
|
radians,
|
||||||
|
@ -13,6 +12,7 @@ import {
|
||||||
segmentDistanceToPoint,
|
segmentDistanceToPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { getCornerRadius, getDiamondPoints } from "../shapes";
|
import { getCornerRadius, getDiamondPoints } from "../shapes";
|
||||||
|
import { createDiamondArc, createDiamondSide } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
@ -22,7 +22,7 @@ import type {
|
||||||
|
|
||||||
export const distanceToBindableElement = (
|
export const distanceToBindableElement = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
point: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
|
@ -32,11 +32,11 @@ export const distanceToBindableElement = (
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
return distanceToRectangleElement(element, point);
|
return distanceToRectangleElement(element, p);
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return distanceToDiamondElement(element, point);
|
return distanceToDiamondElement(element, p);
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return distanceToEllipseElement(element, point);
|
return distanceToEllipseElement(element, p);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -118,54 +118,6 @@ export const distanceToRectangleElement = (
|
||||||
return Math.min(...[...sideDistances, ...cornerDistances]);
|
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
|
* Returns the distance of a point and the provided diamond element, accounting
|
||||||
* for roundness and rotation
|
* for roundness and rotation
|
||||||
|
|
|
@ -1043,7 +1043,6 @@ const getSnapPoint = (
|
||||||
isRectanguloidElement(element) ? avoidRectangularCorner(element, p) : p,
|
isRectanguloidElement(element) ? avoidRectangularCorner(element, p) : p,
|
||||||
otherPoint,
|
otherPoint,
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getBindPointHeading = (
|
const getBindPointHeading = (
|
||||||
|
|
|
@ -474,7 +474,10 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
||||||
return 0;
|
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
|
// Here we add +1 to avoid these numbers to be 0
|
||||||
// otherwise rough.js will throw an error complaining about it
|
// otherwise rough.js will throw an error complaining about it
|
||||||
const topX = Math.floor(element.width / 2) + 1;
|
const topX = Math.floor(element.width / 2) + 1;
|
||||||
|
@ -486,5 +489,14 @@ export const getDiamondPoints = (element: ExcalidrawDiamondElement) => {
|
||||||
const leftX = 0;
|
const leftX = 0;
|
||||||
const leftY = rightY;
|
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",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 99,
|
"height": "121.17708",
|
||||||
"id": "id166",
|
"id": "id166",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -208,8 +208,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.20800",
|
"120.20767",
|
||||||
99,
|
"121.17708",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
@ -224,7 +224,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 40,
|
"version": 40,
|
||||||
"width": "98.20800",
|
"width": "120.20767",
|
||||||
"x": 1,
|
"x": 1,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
|
@ -292,24 +292,24 @@ History {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id165",
|
"elementId": "id165",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": "0.00990",
|
"focus": "0.01000",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "1.37272",
|
"height": "1.78061",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
98,
|
"128.08994",
|
||||||
"-1.37272",
|
"-1.78061",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id164",
|
"elementId": "id164",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": "0.02970",
|
"focus": "0.03001",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -320,15 +320,15 @@ History {
|
||||||
"focus": "-0.02000",
|
"focus": "-0.02000",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.00473",
|
"height": "0.00968",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
302,
|
||||||
"0.00473",
|
"-0.00968",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
|
@ -390,15 +390,15 @@ History {
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": 99,
|
"height": "121.17708",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.20800",
|
"120.20767",
|
||||||
99,
|
"121.17708",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
|
@ -408,27 +408,27 @@ History {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id165",
|
"elementId": "id165",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": "0.00990",
|
"focus": "0.01000",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "1.37680",
|
"height": "1.02669",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"128.74676",
|
||||||
"-1.37680",
|
"-1.02669",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id164",
|
"elementId": "id164",
|
||||||
"fixedPoint": null,
|
"fixedPoint": null,
|
||||||
"focus": "0.02970",
|
"focus": "0.03001",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"y": "1.39313",
|
"y": "0.48108",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id169" => Delta {
|
"id169" => Delta {
|
||||||
|
@ -819,7 +819,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 30,
|
"version": 30,
|
||||||
"width": 0,
|
"width": 0,
|
||||||
"x": 200,
|
"x": "174.50000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1237,7 +1237,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "2.61991",
|
"height": "0.13739",
|
||||||
"id": "id172",
|
"id": "id172",
|
||||||
"index": "Zz",
|
"index": "Zz",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -1251,8 +1251,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"124.66911",
|
||||||
"-2.61991",
|
"0.13739",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
@ -1275,9 +1275,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"width": "98.00000",
|
"width": "124.66911",
|
||||||
"x": "1.00000",
|
"x": 1,
|
||||||
"y": "3.98333",
|
"y": "-0.16421",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -1605,7 +1605,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "2.61991",
|
"height": "0.13739",
|
||||||
"id": "id175",
|
"id": "id175",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -1619,8 +1619,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"124.66911",
|
||||||
"-2.61991",
|
"0.13739",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
@ -1643,9 +1643,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"width": "98.00000",
|
"width": "124.66911",
|
||||||
"x": "1.00000",
|
"x": 1,
|
||||||
"y": "3.98333",
|
"y": "-0.16421",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -1763,7 +1763,7 @@ History {
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "22.36242",
|
"height": "5.44620",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
|
@ -1776,8 +1776,8 @@ History {
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
"188.94246",
|
||||||
"-22.36242",
|
"5.44620",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
@ -1798,9 +1798,9 @@ History {
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": "98.00000",
|
"width": "188.94246",
|
||||||
"x": 1,
|
"x": "-59.03817",
|
||||||
"y": 34,
|
"y": "-6.02545",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -2311,7 +2311,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "408.19672",
|
"height": "414.71403",
|
||||||
"id": "id180",
|
"id": "id180",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -2325,8 +2325,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
498,
|
"576.45250",
|
||||||
"-408.19672",
|
"-414.71403",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
@ -2346,8 +2346,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": 498,
|
"width": "576.45250",
|
||||||
"x": 1,
|
"x": "-75.50000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -14997,7 +14997,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
200,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -15018,8 +15018,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": 200,
|
||||||
"x": 1,
|
"x": "-75.50000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -15693,7 +15693,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
200,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -15714,8 +15714,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": 200,
|
||||||
"x": 1,
|
"x": "-75.50000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -16313,7 +16313,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
200,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -16334,8 +16334,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": 200,
|
||||||
"x": 1,
|
"x": "-75.50000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -16931,7 +16931,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
200,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -16952,8 +16952,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 10,
|
"version": 10,
|
||||||
"width": "98.00000",
|
"width": 200,
|
||||||
"x": 1,
|
"x": "-75.50000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -17646,7 +17646,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.00000",
|
200,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -17667,8 +17667,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"width": "98.00000",
|
"width": 200,
|
||||||
"x": 1,
|
"x": "-75.50000",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -49,9 +49,3 @@ exports[`Test Linear Elements > Test bound text element > should wrap the bound
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made easy"
|
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,
|
"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 { 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 { point } from "./point";
|
||||||
import { segment } from "./segment";
|
import { segment } from "./segment";
|
||||||
|
|
||||||
|
@ -31,7 +37,7 @@ describe("point on arc", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("intersection", () => {
|
describe("intersection", () => {
|
||||||
it("should report correct interception point", () => {
|
it("should report correct interception point for segment", () => {
|
||||||
expect(
|
expect(
|
||||||
arcSegmentInterceptPoints(
|
arcSegmentInterceptPoints(
|
||||||
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
|
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)]);
|
).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(
|
expect(
|
||||||
arcSegmentInterceptPoints(
|
arcSegmentInterceptPoints(
|
||||||
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
|
arc(point(0, 0), 1, radians(-Math.PI / 4), radians(Math.PI / 4)),
|
||||||
|
@ -51,4 +57,25 @@ describe("intersection", () => {
|
||||||
point(0.9, 0.4358898943540668),
|
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 {
|
import {
|
||||||
ellipse,
|
ellipse,
|
||||||
ellipseDistanceFromPoint,
|
ellipseDistanceFromPoint,
|
||||||
|
ellipseLineIntersectionPoints,
|
||||||
ellipseSegmentInterceptPoints,
|
ellipseSegmentInterceptPoints,
|
||||||
} from "./ellipse";
|
} from "./ellipse";
|
||||||
import { point, pointDistance } from "./point";
|
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";
|
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
|
* 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>(
|
export function arcSegmentInterceptPoints<Point extends GenericPoint>(
|
||||||
a: Readonly<Arc<Point>>,
|
a: Readonly<Arc<Point>>,
|
||||||
|
@ -106,3 +107,31 @@ export function arcSegmentInterceptPoints<Point extends GenericPoint>(
|
||||||
: a.startAngle <= candidateAngle || a.endAngle >= candidateAngle;
|
: 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,
|
ellipseSegmentInterceptPoints,
|
||||||
ellipseIncludesPoint,
|
ellipseIncludesPoint,
|
||||||
ellipseTouchesPoint,
|
ellipseTouchesPoint,
|
||||||
ellipseIntersectsLine,
|
ellipseLineIntersectionPoints,
|
||||||
} from "./ellipse";
|
} from "./ellipse";
|
||||||
import { line } from "./line";
|
import { line } from "./line";
|
||||||
import { point } from "./point";
|
import { point } from "./point";
|
||||||
|
@ -85,7 +85,7 @@ describe("line and ellipse", () => {
|
||||||
|
|
||||||
it("detects outside line", () => {
|
it("detects outside line", () => {
|
||||||
expect(
|
expect(
|
||||||
ellipseIntersectsLine(
|
ellipseLineIntersectionPoints(
|
||||||
e,
|
e,
|
||||||
line<GlobalPoint>(point(-10, -10), point(10, -10)),
|
line<GlobalPoint>(point(-10, -10), point(10, -10)),
|
||||||
),
|
),
|
||||||
|
@ -93,10 +93,10 @@ describe("line and ellipse", () => {
|
||||||
});
|
});
|
||||||
it("detects line intersecting ellipse", () => {
|
it("detects line intersecting ellipse", () => {
|
||||||
expect(
|
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)]);
|
).toEqual([point(0, 2), point(0, -2)]);
|
||||||
expect(
|
expect(
|
||||||
ellipseIntersectsLine(
|
ellipseLineIntersectionPoints(
|
||||||
e,
|
e,
|
||||||
line<GlobalPoint>(point(-100, 0), point(-10, 0)),
|
line<GlobalPoint>(point(-100, 0), point(-10, 0)),
|
||||||
).map(([x, y]) => point(Math.round(x), Math.round(y))),
|
).map(([x, y]) => point(Math.round(x), Math.round(y))),
|
||||||
|
@ -104,7 +104,7 @@ describe("line and ellipse", () => {
|
||||||
});
|
});
|
||||||
it("detects line touching ellipse", () => {
|
it("detects line touching ellipse", () => {
|
||||||
expect(
|
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)]);
|
).toEqual([point(0, -2)]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -181,7 +181,7 @@ export function ellipseSegmentInterceptPoints<Point extends GenericPoint>(
|
||||||
return intersections;
|
return intersections;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ellipseIntersectsLine<Point extends GenericPoint>(
|
export function ellipseLineIntersectionPoints<Point extends GenericPoint>(
|
||||||
{ center, halfWidth, halfHeight }: Ellipse<Point>,
|
{ center, halfWidth, halfHeight }: Ellipse<Point>,
|
||||||
[g, h]: Line<Point>,
|
[g, h]: Line<Point>,
|
||||||
): Point[] {
|
): Point[] {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { line, lineIntersectsLine, lineIntersectsSegment } from "./line";
|
import { line, lineLineIntersectionPoint, lineSegmentIntersectionPoints } from "./line";
|
||||||
import { point } from "./point";
|
import { point } from "./point";
|
||||||
import { segment } from "./segment";
|
import { segment } from "./segment";
|
||||||
|
|
||||||
describe("line-line intersections", () => {
|
describe("line-line intersections", () => {
|
||||||
it("should correctly detect intersection at origin", () => {
|
it("should correctly detect intersection at origin", () => {
|
||||||
expect(
|
expect(
|
||||||
lineIntersectsLine(
|
lineLineIntersectionPoint(
|
||||||
line(point(-5, -5), point(5, 5)),
|
line(point(-5, -5), point(5, 5)),
|
||||||
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", () => {
|
it("should correctly detect intersection at non-origin", () => {
|
||||||
expect(
|
expect(
|
||||||
lineIntersectsLine(
|
lineLineIntersectionPoint(
|
||||||
line(point(0, 0), point(10, 10)),
|
line(point(0, 0), point(10, 10)),
|
||||||
line(point(10, 0), point(0, 10)),
|
line(point(10, 0), point(0, 10)),
|
||||||
),
|
),
|
||||||
|
@ -23,7 +23,7 @@ describe("line-line intersections", () => {
|
||||||
|
|
||||||
it("should correctly detect parallel lines", () => {
|
it("should correctly detect parallel lines", () => {
|
||||||
expect(
|
expect(
|
||||||
lineIntersectsLine(
|
lineLineIntersectionPoint(
|
||||||
line(point(0, 0), point(0, 10)),
|
line(point(0, 0), point(0, 10)),
|
||||||
line(point(10, 0), point(10, 10)),
|
line(point(10, 0), point(10, 10)),
|
||||||
),
|
),
|
||||||
|
@ -34,7 +34,7 @@ describe("line-line intersections", () => {
|
||||||
describe("line-segment intersections", () => {
|
describe("line-segment intersections", () => {
|
||||||
it("should correctly detect intersection", () => {
|
it("should correctly detect intersection", () => {
|
||||||
expect(
|
expect(
|
||||||
lineIntersectsSegment(
|
lineSegmentIntersectionPoints(
|
||||||
line(point(0, 0), point(5, 0)),
|
line(point(0, 0), point(5, 0)),
|
||||||
segment(point(2, -2), point(3, 2)),
|
segment(point(2, -2), point(3, 2)),
|
||||||
),
|
),
|
||||||
|
@ -42,7 +42,7 @@ describe("line-segment intersections", () => {
|
||||||
});
|
});
|
||||||
it("should correctly detect non-intersection", () => {
|
it("should correctly detect non-intersection", () => {
|
||||||
expect(
|
expect(
|
||||||
lineIntersectsSegment(
|
lineSegmentIntersectionPoints(
|
||||||
line(point(0, 0), point(5, 0)),
|
line(point(0, 0), point(5, 0)),
|
||||||
segment(point(3, 1), point(4, 4)),
|
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 { point, pointCenter, pointRotateRads } from "./point";
|
||||||
import { segmentIncludesPoint } from "./segment";
|
import { segmentIncludesPoint } from "./segment";
|
||||||
import type { GenericPoint, Line, Radians, Segment } from "./types";
|
import type { GenericPoint, Line, Radians, Segment } from "./types";
|
||||||
|
@ -60,7 +60,7 @@ export function lineRotate<Point extends GenericPoint>(
|
||||||
* @param b Another line to intersect
|
* @param b Another line to intersect
|
||||||
* @returns The intersection point
|
* @returns The intersection point
|
||||||
*/
|
*/
|
||||||
export function lineIntersectsLine<Point extends GenericPoint>(
|
export function lineLineIntersectionPoint<Point extends GenericPoint>(
|
||||||
[[x1, y1], [x2, y2]]: Line<Point>,
|
[[x1, y1], [x2, y2]]: Line<Point>,
|
||||||
[[x3, y3], [x4, y4]]: Line<Point>,
|
[[x3, y3], [x4, y4]]: Line<Point>,
|
||||||
): Point | null {
|
): Point | null {
|
||||||
|
@ -83,11 +83,11 @@ export function lineIntersectsLine<Point extends GenericPoint>(
|
||||||
* @param s
|
* @param s
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function lineIntersectsSegment<Point extends GenericPoint>(
|
export function lineSegmentIntersectionPoints<Point extends GenericPoint>(
|
||||||
l: Line<Point>,
|
l: Line<Point>,
|
||||||
s: Segment<Point>,
|
s: Segment<Point>,
|
||||||
): Point | null {
|
): 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)) {
|
if (!candidate || !segmentIncludesPoint(candidate, s)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -95,4 +95,4 @@ export function lineIntersectsSegment<Point extends GenericPoint>(
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const lineInterceptsEllipse = ellipseIntersectsLine;
|
export const lineInterceptsEllipse = ellipseLineIntersectionPoints;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { lineIntersectsSegment } from "./line";
|
import { lineSegmentIntersectionPoints } from "./line";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
pointCenter,
|
pointCenter,
|
||||||
|
@ -186,4 +186,4 @@ export function segmentDistanceToPoint<Point extends GenericPoint>(
|
||||||
* @param s
|
* @param s
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const segmentIntersectsLine = lineIntersectsSegment;
|
export const segmentLineIntersectionPoints = lineSegmentIntersectionPoints;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue