Replace intersection code

This commit is contained in:
Mark Tolmacs 2024-10-01 16:24:31 +02:00
parent 411deae176
commit 0c02972695
No known key found for this signature in database
18 changed files with 507 additions and 687 deletions

View file

@ -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([

View file

@ -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)),
);
};

View file

@ -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));
};

View file

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

View file

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