mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
First implementation of element distance functions with failing tests
This commit is contained in:
parent
47cc842415
commit
d9ea7190ec
6 changed files with 275 additions and 173 deletions
|
@ -25,7 +25,6 @@ import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
ExcalidrawRectanguloidElement,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
|
@ -63,7 +62,7 @@ import {
|
||||||
vectorToHeading,
|
vectorToHeading,
|
||||||
type Heading,
|
type Heading,
|
||||||
} from "./heading";
|
} from "./heading";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import {
|
import {
|
||||||
segment,
|
segment,
|
||||||
point,
|
point,
|
||||||
|
@ -76,6 +75,7 @@ import {
|
||||||
radians,
|
radians,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
||||||
|
import { distanceToBindableElement } from "./distance";
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
| NonDeleted<ExcalidrawBindableElement>
|
| NonDeleted<ExcalidrawBindableElement>
|
||||||
|
@ -557,10 +557,7 @@ const calculateFocusAndGap = (
|
||||||
edgePoint,
|
edgePoint,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
),
|
||||||
gap: Math.max(
|
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
||||||
1,
|
|
||||||
distanceToBindableElement(hoveredElement, edgePoint, elementsMap),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -736,11 +733,7 @@ const getDistanceForBinding = (
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const distance = distanceToBindableElement(
|
const distance = distanceToBindableElement(bindableElement, point);
|
||||||
bindableElement,
|
|
||||||
point,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
const bindDistance = maxBindingGap(
|
const bindDistance = maxBindingGap(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
bindableElement.width,
|
bindableElement.width,
|
||||||
|
@ -781,9 +774,7 @@ export const bindPointToSnapToElementOutline = (
|
||||||
const isVertical =
|
const isVertical =
|
||||||
compareHeading(heading, HEADING_LEFT) ||
|
compareHeading(heading, HEADING_LEFT) ||
|
||||||
compareHeading(heading, HEADING_RIGHT);
|
compareHeading(heading, HEADING_RIGHT);
|
||||||
const dist = Math.abs(
|
const dist = Math.abs(distanceToBindableElement(bindableElement, p));
|
||||||
distanceToBindableElement(bindableElement, p, elementsMap),
|
|
||||||
);
|
|
||||||
const isInner = isVertical
|
const isInner = isVertical
|
||||||
? dist < bindableElement.width * -0.1
|
? dist < bindableElement.width * -0.1
|
||||||
: dist < bindableElement.height * -0.1;
|
: dist < bindableElement.height * -0.1;
|
||||||
|
@ -937,7 +928,7 @@ export const snapToMid = (
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
|
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
|
||||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
const nonRotated = pointRotateRads(p, center, radians(-angle));
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
// above and below certain px distance
|
// above and below certain px distance
|
||||||
|
@ -1123,7 +1114,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||||
const nonRotatedSnappedGlobalPoint = pointRotateRads(
|
const nonRotatedSnappedGlobalPoint = pointRotateRads(
|
||||||
snappedPoint,
|
snappedPoint,
|
||||||
globalMidPoint,
|
globalMidPoint,
|
||||||
-hoveredElement.angle as Radians,
|
radians(-hoveredElement.angle),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1351,148 +1342,6 @@ export const maxBindingGap = (
|
||||||
return Math.max(16, Math.min(0.25 * smallerDimension, 32));
|
return Math.max(16, Math.min(0.25 * smallerDimension, 32));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const distanceToBindableElement = (
|
|
||||||
element: ExcalidrawBindableElement,
|
|
||||||
point: GlobalPoint,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): number => {
|
|
||||||
switch (element.type) {
|
|
||||||
case "rectangle":
|
|
||||||
case "image":
|
|
||||||
case "text":
|
|
||||||
case "iframe":
|
|
||||||
case "embeddable":
|
|
||||||
case "frame":
|
|
||||||
case "magicframe":
|
|
||||||
return distanceToRectangle(element, point, elementsMap);
|
|
||||||
case "diamond":
|
|
||||||
return distanceToDiamond(element, point, elementsMap);
|
|
||||||
case "ellipse":
|
|
||||||
return distanceToEllipse(element, point, elementsMap);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const distanceToRectangle = (
|
|
||||||
element: ExcalidrawRectanguloidElement,
|
|
||||||
p: GlobalPoint,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): number => {
|
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
|
||||||
element,
|
|
||||||
p,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
return Math.max(
|
|
||||||
GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)),
|
|
||||||
GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const distanceToDiamond = (
|
|
||||||
element: ExcalidrawDiamondElement,
|
|
||||||
point: GlobalPoint,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): number => {
|
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
|
||||||
element,
|
|
||||||
point,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
const side = GALine.equation(hheight, hwidth, -hheight * hwidth);
|
|
||||||
return GAPoint.distanceToLine(pointRel, side);
|
|
||||||
};
|
|
||||||
|
|
||||||
const distanceToEllipse = (
|
|
||||||
element: ExcalidrawEllipseElement,
|
|
||||||
point: GlobalPoint,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): number => {
|
|
||||||
const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
|
|
||||||
return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ellipseParamsForTest = (
|
|
||||||
element: ExcalidrawEllipseElement,
|
|
||||||
point: GlobalPoint,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): [GA.Point, GA.Line] => {
|
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
|
||||||
element,
|
|
||||||
point,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
const [px, py] = GAPoint.toTuple(pointRel);
|
|
||||||
|
|
||||||
// We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)`
|
|
||||||
let tx = 0.707;
|
|
||||||
let ty = 0.707;
|
|
||||||
|
|
||||||
const a = hwidth;
|
|
||||||
const b = hheight;
|
|
||||||
|
|
||||||
// This is a numerical method to find the params tx, ty at which
|
|
||||||
// the ellipse has the closest point to the given point
|
|
||||||
[0, 1, 2, 3].forEach((_) => {
|
|
||||||
const xx = a * tx;
|
|
||||||
const yy = b * ty;
|
|
||||||
|
|
||||||
const ex = ((a * a - b * b) * tx ** 3) / a;
|
|
||||||
const ey = ((b * b - a * a) * ty ** 3) / b;
|
|
||||||
|
|
||||||
const rx = xx - ex;
|
|
||||||
const ry = yy - ey;
|
|
||||||
|
|
||||||
const qx = px - ex;
|
|
||||||
const qy = py - ey;
|
|
||||||
|
|
||||||
const r = Math.hypot(ry, rx);
|
|
||||||
const q = Math.hypot(qy, qx);
|
|
||||||
|
|
||||||
tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
|
|
||||||
ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
|
|
||||||
const t = Math.hypot(ty, tx);
|
|
||||||
tx /= t;
|
|
||||||
ty /= t;
|
|
||||||
});
|
|
||||||
|
|
||||||
const closestPoint = GA.point(a * tx, b * ty);
|
|
||||||
|
|
||||||
const tangent = GALine.orthogonalThrough(pointRel, closestPoint);
|
|
||||||
return [pointRel, tangent];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns:
|
|
||||||
// 1. the point relative to the elements (x, y) position
|
|
||||||
// 2. the point relative to the element's center with positive (x, y)
|
|
||||||
// 3. half element width
|
|
||||||
// 4. half element height
|
|
||||||
//
|
|
||||||
// Note that for linear elements the (x, y) position is not at the
|
|
||||||
// top right corner of their boundary.
|
|
||||||
//
|
|
||||||
// Rectangles, diamonds and ellipses are symmetrical over axes,
|
|
||||||
// and other elements have a rectangular boundary,
|
|
||||||
// so we only need to perform hit tests for the positive quadrant.
|
|
||||||
const pointRelativeToElement = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
pointTuple: GlobalPoint,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): [GA.Point, GA.Point, number, number] => {
|
|
||||||
const point = GAPoint.from(pointTuple);
|
|
||||||
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 pointRotated = GATransform.apply(rotate, point);
|
|
||||||
const pointRelToCenter = GA.sub(pointRotated, GADirection.from(center));
|
|
||||||
const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
|
|
||||||
const elementPos = GA.offset(element.x, element.y);
|
|
||||||
const pointRelToPos = GA.sub(pointRotated, elementPos);
|
|
||||||
const halfWidth = (x2 - x1) / 2;
|
|
||||||
const halfHeight = (y2 - y1) / 2;
|
|
||||||
return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
|
|
||||||
};
|
|
||||||
|
|
||||||
const relativizationToElementCenter = (
|
const relativizationToElementCenter = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
|
210
packages/excalidraw/element/distance.ts
Normal file
210
packages/excalidraw/element/distance.ts
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
import type { GlobalPoint, Segment } from "../../math";
|
||||||
|
import {
|
||||||
|
arc,
|
||||||
|
arcDistanceFromPoint,
|
||||||
|
ellipse,
|
||||||
|
ellipseDistanceFromPoint,
|
||||||
|
ellipseSegmentInterceptPoints,
|
||||||
|
point,
|
||||||
|
pointRotateRads,
|
||||||
|
radians,
|
||||||
|
rectangle,
|
||||||
|
segment,
|
||||||
|
segmentDistanceToPoint,
|
||||||
|
} from "../../math";
|
||||||
|
import { getCornerRadius } from "../shapes";
|
||||||
|
import type {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const distanceToBindableElement = (
|
||||||
|
element: ExcalidrawBindableElement,
|
||||||
|
point: GlobalPoint,
|
||||||
|
): number => {
|
||||||
|
switch (element.type) {
|
||||||
|
case "rectangle":
|
||||||
|
case "image":
|
||||||
|
case "text":
|
||||||
|
case "iframe":
|
||||||
|
case "embeddable":
|
||||||
|
case "frame":
|
||||||
|
case "magicframe":
|
||||||
|
return distanceToRectangleElement(element, point);
|
||||||
|
case "diamond":
|
||||||
|
return distanceToDiamondElement(element, point);
|
||||||
|
case "ellipse":
|
||||||
|
return distanceToEllipseElement(element, point);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const distanceToRectangleElement = (
|
||||||
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
p: GlobalPoint,
|
||||||
|
) => {
|
||||||
|
const center = point(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
const r = rectangle(
|
||||||
|
pointRotateRads(
|
||||||
|
point(element.x, element.y),
|
||||||
|
center,
|
||||||
|
radians(element.angle),
|
||||||
|
),
|
||||||
|
pointRotateRads(
|
||||||
|
point(element.x + element.width, element.y + element.height),
|
||||||
|
center,
|
||||||
|
radians(element.angle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const roundness = getCornerRadius(
|
||||||
|
Math.min(element.width, element.height),
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
const rotatedPoint = pointRotateRads(p, center, element.angle);
|
||||||
|
const sideDistances = [
|
||||||
|
segment(
|
||||||
|
point(r[0][0] + roundness, r[0][1]),
|
||||||
|
point(r[1][0] - roundness, r[0][1]),
|
||||||
|
),
|
||||||
|
segment(
|
||||||
|
point(r[1][0], r[0][1] + roundness),
|
||||||
|
point(r[1][0], r[1][1] - roundness),
|
||||||
|
),
|
||||||
|
segment(
|
||||||
|
point(r[1][0] - roundness, r[1][1]),
|
||||||
|
point(r[0][0] + roundness, r[1][1]),
|
||||||
|
),
|
||||||
|
segment(
|
||||||
|
point(r[0][0], r[1][1] - roundness),
|
||||||
|
point(r[0][0], r[0][1] + roundness),
|
||||||
|
),
|
||||||
|
].map((s) => segmentDistanceToPoint(rotatedPoint, s));
|
||||||
|
const cornerDistances =
|
||||||
|
roundness > 0
|
||||||
|
? [
|
||||||
|
arc(
|
||||||
|
point(r[0][0] + roundness, r[0][1] + roundness),
|
||||||
|
roundness,
|
||||||
|
radians(Math.PI),
|
||||||
|
radians((3 / 4) * Math.PI),
|
||||||
|
),
|
||||||
|
arc(
|
||||||
|
point(r[1][0] - roundness, r[0][1] + roundness),
|
||||||
|
roundness,
|
||||||
|
radians((3 / 4) * Math.PI),
|
||||||
|
radians(0),
|
||||||
|
),
|
||||||
|
arc(
|
||||||
|
point(r[1][0] - roundness, r[1][1] - roundness),
|
||||||
|
roundness,
|
||||||
|
radians(0),
|
||||||
|
radians((1 / 2) * Math.PI),
|
||||||
|
),
|
||||||
|
arc(
|
||||||
|
point(r[0][0] + roundness, r[1][1] - roundness),
|
||||||
|
roundness,
|
||||||
|
radians((1 / 2) * Math.PI),
|
||||||
|
radians(Math.PI),
|
||||||
|
),
|
||||||
|
].map((a) => arcDistanceFromPoint(a, rotatedPoint))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return Math.min(...[...sideDistances, ...cornerDistances]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundedCutoffSegment = (
|
||||||
|
s: Segment<GlobalPoint>,
|
||||||
|
r: number,
|
||||||
|
): Segment<GlobalPoint> => {
|
||||||
|
const t = (4 * r) / Math.sqrt(2);
|
||||||
|
|
||||||
|
return segment(
|
||||||
|
ellipseSegmentInterceptPoints(ellipse(s[0], radians(0), t, t), s)[0],
|
||||||
|
ellipseSegmentInterceptPoints(ellipse(s[1], radians(0), t, t), s)[0],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const diamondArc = (left: GlobalPoint, right: GlobalPoint, r: number) => {
|
||||||
|
const c = point((left[0] + right[0]) / 2, left[1]);
|
||||||
|
|
||||||
|
return arc(
|
||||||
|
c,
|
||||||
|
r,
|
||||||
|
radians(Math.asin((left[1] - c[1]) / r)),
|
||||||
|
radians(Math.asin((right[1] - c[1]) / r)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const distanceToDiamondElement = (
|
||||||
|
element: ExcalidrawDiamondElement,
|
||||||
|
p: GlobalPoint,
|
||||||
|
): number => {
|
||||||
|
const center = point<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
);
|
||||||
|
const roundness = getCornerRadius(
|
||||||
|
Math.min(element.width, element.height),
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
const rotatedPoint = pointRotateRads(p, center, element.angle);
|
||||||
|
const top = pointRotateRads<GlobalPoint>(
|
||||||
|
point(element.x + element.width / 2, element.y),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const right = pointRotateRads<GlobalPoint>(
|
||||||
|
point(element.x + element.width, element.y + element.height / 2),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const bottom = pointRotateRads<GlobalPoint>(
|
||||||
|
point(element.x + element.width / 2, element.y + element.height),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const left = pointRotateRads<GlobalPoint>(
|
||||||
|
point(element.x, element.y + element.height / 2),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const topRight = roundedCutoffSegment(segment(top, right), roundness);
|
||||||
|
const bottomRight = roundedCutoffSegment(segment(right, bottom), roundness);
|
||||||
|
const bottomLeft = roundedCutoffSegment(segment(bottom, left), roundness);
|
||||||
|
const topLeft = roundedCutoffSegment(segment(left, top), roundness);
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
...[
|
||||||
|
...[topRight, bottomRight, bottomLeft, topLeft].map((s) =>
|
||||||
|
segmentDistanceToPoint(rotatedPoint, s),
|
||||||
|
),
|
||||||
|
...(roundness > 0
|
||||||
|
? [
|
||||||
|
diamondArc(topLeft[1], topRight[0], roundness),
|
||||||
|
diamondArc(topRight[1], bottomRight[0], roundness),
|
||||||
|
diamondArc(bottomRight[1], bottomLeft[0], roundness),
|
||||||
|
diamondArc(bottomLeft[1], topLeft[0], roundness),
|
||||||
|
].map((a) => arcDistanceFromPoint(a, rotatedPoint))
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const distanceToEllipseElement = (
|
||||||
|
element: ExcalidrawEllipseElement,
|
||||||
|
p: GlobalPoint,
|
||||||
|
): number => {
|
||||||
|
return ellipseDistanceFromPoint(
|
||||||
|
p,
|
||||||
|
ellipse(
|
||||||
|
point(element.x + element.width / 2, element.y + element.height / 2),
|
||||||
|
element.angle,
|
||||||
|
element.width / 2,
|
||||||
|
element.height / 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
|
@ -140,10 +140,10 @@ export const findShapeByKey = (key: string) => {
|
||||||
* get the pure geometric shape of an excalidraw element
|
* get the pure geometric shape of an excalidraw element
|
||||||
* which is then used for hit detection
|
* which is then used for hit detection
|
||||||
*/
|
*/
|
||||||
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getElementShape = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): GeometricShape<Point> => {
|
): GeometricShape<GlobalPoint> => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
@ -163,16 +163,16 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
|
|
||||||
return shouldTestInside(element)
|
return shouldTestInside(element)
|
||||||
? getClosedCurveShape<Point>(
|
? getClosedCurveShape<GlobalPoint>(
|
||||||
element,
|
element,
|
||||||
roughShape,
|
roughShape,
|
||||||
point<Point>(element.x, element.y),
|
point<GlobalPoint>(element.x, element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
point(cx, cy),
|
point(cx, cy),
|
||||||
)
|
)
|
||||||
: getCurveShape<Point>(
|
: getCurveShape<GlobalPoint>(
|
||||||
roughShape,
|
roughShape,
|
||||||
point<Point>(element.x, element.y),
|
point<GlobalPoint>(element.x, element.y),
|
||||||
element.angle,
|
element.angle,
|
||||||
point(cx, cy),
|
point(cx, cy),
|
||||||
);
|
);
|
||||||
|
@ -192,10 +192,10 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getBoundTextShape = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): GeometricShape<Point> | null => {
|
): GeometricShape<GlobalPoint> | null => {
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { invariant } from "../excalidraw/utils";
|
||||||
import { cartesian2Polar, radians } from "./angle";
|
import { cartesian2Polar, radians } from "./angle";
|
||||||
import { ellipse, ellipseSegmentInterceptPoints } from "./ellipse";
|
import { ellipse, ellipseSegmentInterceptPoints } from "./ellipse";
|
||||||
import { point } from "./point";
|
import { point, pointDistance } from "./point";
|
||||||
|
import { segment } from "./segment";
|
||||||
import type { GenericPoint, Segment, Radians, Arc } from "./types";
|
import type { GenericPoint, Segment, Radians, Arc } from "./types";
|
||||||
import { PRECISION } from "./utils";
|
import { PRECISION } from "./utils";
|
||||||
|
|
||||||
|
@ -42,6 +44,25 @@ export function arcIncludesPoint<P extends GenericPoint>(
|
||||||
: startAngle <= angle || endAngle >= angle;
|
: startAngle <= angle || endAngle >= angle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param a
|
||||||
|
* @param p
|
||||||
|
*/
|
||||||
|
export function arcDistanceFromPoint<Point extends GenericPoint>(
|
||||||
|
a: Arc<Point>,
|
||||||
|
p: Point,
|
||||||
|
) {
|
||||||
|
const intersectPoint = arcSegmentInterceptPoint(a, segment(p, a.center));
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
intersectPoint.length !== 1,
|
||||||
|
"Arc distance intersector cannot have multiple intersections",
|
||||||
|
);
|
||||||
|
|
||||||
|
return pointDistance(intersectPoint[0], p);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { pointsEqual } from "./point";
|
import { pointsEqual } from "./point";
|
||||||
import { segment, segmentIncludesPoint } from "./segment";
|
import { segment, segmentIncludesPoint, segmentsIntersectAt } from "./segment";
|
||||||
import type { GenericPoint, Polygon } from "./types";
|
import type { GenericPoint, Polygon, Segment } from "./types";
|
||||||
import { PRECISION } from "./utils";
|
import { PRECISION } from "./utils";
|
||||||
|
|
||||||
export function polygon<Point extends GenericPoint>(...points: Point[]) {
|
export function polygon<Point extends GenericPoint>(...points: Point[]) {
|
||||||
|
@ -62,3 +62,25 @@ function polygonClose<Point extends GenericPoint>(polygon: Point[]) {
|
||||||
function polygonIsClosed<Point extends GenericPoint>(polygon: Point[]) {
|
function polygonIsClosed<Point extends GenericPoint>(polygon: Point[]) {
|
||||||
return pointsEqual(polygon[0], polygon[polygon.length - 1]);
|
return pointsEqual(polygon[0], polygon[polygon.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the points of intersection of a line segment, identified by exactly
|
||||||
|
* one start pointand one end point, and the polygon identified by a set of
|
||||||
|
* ponits representing a set of connected lines.
|
||||||
|
*/
|
||||||
|
export function polygonSegmentIntersectionPoints<Point extends GenericPoint>(
|
||||||
|
polygon: Readonly<Polygon<Point>>,
|
||||||
|
segment: Readonly<Segment<Point>>,
|
||||||
|
): Point[] {
|
||||||
|
return polygon
|
||||||
|
.reduce((segments, current, idx, poly) => {
|
||||||
|
return idx === 0
|
||||||
|
? []
|
||||||
|
: ([
|
||||||
|
...segments,
|
||||||
|
[poly[idx - 1] as Point, current],
|
||||||
|
] as Segment<Point>[]);
|
||||||
|
}, [] as Segment<Point>[])
|
||||||
|
.map((s) => segmentsIntersectAt(s, segment))
|
||||||
|
.filter((point) => point !== null) as Point[];
|
||||||
|
}
|
||||||
|
|
|
@ -122,11 +122,11 @@ export const segmentIncludesPoint = <Point extends GenericPoint>(
|
||||||
};
|
};
|
||||||
|
|
||||||
export const segmentDistanceToPoint = <Point extends GenericPoint>(
|
export const segmentDistanceToPoint = <Point extends GenericPoint>(
|
||||||
point: Point,
|
p: Point,
|
||||||
line: Segment<Point>,
|
s: Segment<Point>,
|
||||||
) => {
|
) => {
|
||||||
const [x, y] = point;
|
const [x, y] = p;
|
||||||
const [[x1, y1], [x2, y2]] = line;
|
const [[x1, y1], [x2, y2]] = s;
|
||||||
|
|
||||||
const A = x - x1;
|
const A = x - x1;
|
||||||
const B = y - y1;
|
const B = y - y1;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue