Revert "State"
This reverts commit 4ba1bdaead0d74afea3c151efedeb59dfddf24de.
This commit is contained in:
parent
4ba1bdaead
commit
da9cda14d6
@ -1,8 +1,17 @@
|
||||
import { simplify } from "points-on-curve";
|
||||
|
||||
import { pointFrom, type LocalPoint } from "@excalidraw/math";
|
||||
import {
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
type LocalPoint,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
|
||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||
@ -16,11 +25,15 @@ import {
|
||||
isLinearElement,
|
||||
} from "./typeChecks";
|
||||
import { getCornerRadius, isPathALoop } from "./shapes";
|
||||
import { generateElbowArrowRougJshPathCommands } from "./utils";
|
||||
import { headingForPointIsHorizontal } from "./heading";
|
||||
|
||||
import { canChangeRoundness } from "./comparisons";
|
||||
import { generateFreeDrawShape } from "./renderElement";
|
||||
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
||||
import {
|
||||
getArrowheadPoints,
|
||||
getDiamondPoints,
|
||||
getElementBounds,
|
||||
} from "./bounds";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
@ -28,11 +41,11 @@ import type {
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
ExcalidrawFreeDrawElement,
|
||||
} from "./types";
|
||||
|
||||
import type { Drawable, Options } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||
|
||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||
|
||||
@ -303,6 +316,125 @@ const getArrowheadShapes = (
|
||||
}
|
||||
};
|
||||
|
||||
export const generateLinearCollisionShape = (
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
) => {
|
||||
const generator = new RoughGenerator();
|
||||
const options: Options = {
|
||||
seed: element.seed,
|
||||
disableMultiStroke: true,
|
||||
disableMultiStrokeFill: true,
|
||||
roughness: 0,
|
||||
preserveVertices: true,
|
||||
};
|
||||
|
||||
switch (element.type) {
|
||||
case "line":
|
||||
case "arrow": {
|
||||
// points array can be empty in the beginning, so it is important to add
|
||||
// initial position to it
|
||||
const points = element.points.length
|
||||
? element.points
|
||||
: [pointFrom<LocalPoint>(0, 0)];
|
||||
const [x1, y1, x2, y2] = getElementBounds(
|
||||
{
|
||||
...element,
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
new Map(),
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
return generator.path(generateElbowArrowShape(points, 16), options)
|
||||
.sets[0].ops;
|
||||
} else if (!element.roundness) {
|
||||
return points.map((point, idx) => {
|
||||
const p = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x + point[0], element.y + point[1]),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
return {
|
||||
op: idx === 0 ? "move" : "lineTo",
|
||||
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return generator
|
||||
.curve(points as unknown as RoughPoint[], options)
|
||||
.sets[0].ops.slice(0, element.points.length)
|
||||
.map((op, i, arr) => {
|
||||
if (i === 0) {
|
||||
const p = pointRotateRads<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
return {
|
||||
op: "move",
|
||||
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
op: "bcurveTo",
|
||||
data: [
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[2],
|
||||
element.y + op.data[3],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[4],
|
||||
element.y + op.data[5],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
]
|
||||
.map((p) =>
|
||||
pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
)
|
||||
.flat(),
|
||||
};
|
||||
});
|
||||
}
|
||||
case "freedraw": {
|
||||
if (element.points.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const simplifiedPoints = simplify(
|
||||
element.points as Mutable<LocalPoint[]>,
|
||||
0.75,
|
||||
);
|
||||
|
||||
return generator
|
||||
.curve(simplifiedPoints as [number, number][], options)
|
||||
.sets[0].ops.slice(0, element.points.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the roughjs shape for given element.
|
||||
*
|
||||
@ -452,7 +584,7 @@ export const _generateElementShape = (
|
||||
} else {
|
||||
shape = [
|
||||
generator.path(
|
||||
generateElbowArrowRougJshPathCommands(points, 16),
|
||||
generateElbowArrowShape(points, 16),
|
||||
generateRoughOptions(element, true),
|
||||
),
|
||||
];
|
||||
@ -546,3 +678,68 @@ export const _generateElementShape = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const generateElbowArrowShape = (
|
||||
points: readonly LocalPoint[],
|
||||
radius: number,
|
||||
) => {
|
||||
const subpoints = [] as [number, number][];
|
||||
for (let i = 1; i < points.length - 1; i += 1) {
|
||||
const prev = points[i - 1];
|
||||
const next = points[i + 1];
|
||||
const point = points[i];
|
||||
const prevIsHorizontal = headingForPointIsHorizontal(point, prev);
|
||||
const nextIsHorizontal = headingForPointIsHorizontal(next, point);
|
||||
const corner = Math.min(
|
||||
radius,
|
||||
pointDistance(points[i], next) / 2,
|
||||
pointDistance(points[i], prev) / 2,
|
||||
);
|
||||
|
||||
if (prevIsHorizontal) {
|
||||
if (prev[0] < point[0]) {
|
||||
// LEFT
|
||||
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||
} else {
|
||||
// RIGHT
|
||||
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||
}
|
||||
} else if (prev[1] < point[1]) {
|
||||
// UP
|
||||
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||
} else {
|
||||
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||
}
|
||||
|
||||
subpoints.push(points[i] as [number, number]);
|
||||
|
||||
if (nextIsHorizontal) {
|
||||
if (next[0] < point[0]) {
|
||||
// LEFT
|
||||
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||
} else {
|
||||
// RIGHT
|
||||
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||
}
|
||||
} else if (next[1] < point[1]) {
|
||||
// UP
|
||||
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||
} else {
|
||||
// DOWN
|
||||
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||
}
|
||||
}
|
||||
|
||||
const d = [`M ${points[0][0]} ${points[0][1]}`];
|
||||
for (let i = 0; i < subpoints.length; i += 3) {
|
||||
d.push(`L ${subpoints[i][0]} ${subpoints[i][1]}`);
|
||||
d.push(
|
||||
`Q ${subpoints[i + 1][0]} ${subpoints[i + 1][1]}, ${
|
||||
subpoints[i + 2][0]
|
||||
} ${subpoints[i + 2][1]}`,
|
||||
);
|
||||
}
|
||||
d.push(`L ${points[points.length - 1][0]} ${points[points.length - 1][1]}`);
|
||||
|
||||
return d.join(" ");
|
||||
};
|
||||
|
@ -226,7 +226,10 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
||||
: linearElement.endBinding?.elementId;
|
||||
if (elementId) {
|
||||
const element = elementsMap.get(elementId);
|
||||
if (isBindableElement(element) && bindingBorderTest(element, coors, zoom)) {
|
||||
if (
|
||||
isBindableElement(element) &&
|
||||
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@ -556,6 +559,7 @@ export const getHoveredElementForBinding = (
|
||||
bindingBorderTest(
|
||||
element,
|
||||
pointerCoords,
|
||||
elementsMap,
|
||||
zoom,
|
||||
(fullShape ||
|
||||
!isBindingFallthroughEnabled(
|
||||
@ -588,7 +592,7 @@ export const getHoveredElementForBinding = (
|
||||
|
||||
// Prefer the shape with the border being tested (if any)
|
||||
const borderTestElements = candidateElements.filter((element) =>
|
||||
bindingBorderTest(element, pointerCoords, zoom, false),
|
||||
bindingBorderTest(element, pointerCoords, elementsMap, zoom, false),
|
||||
);
|
||||
if (borderTestElements.length === 1) {
|
||||
return borderTestElements[0];
|
||||
@ -609,6 +613,7 @@ export const getHoveredElementForBinding = (
|
||||
bindingBorderTest(
|
||||
element,
|
||||
pointerCoords,
|
||||
elementsMap,
|
||||
zoom,
|
||||
// disable fullshape snapping for frame elements so we
|
||||
// can bind to frame children
|
||||
@ -1540,6 +1545,7 @@ const newBoundElements = (
|
||||
export const bindingBorderTest = (
|
||||
element: NonDeleted<ExcalidrawBindableElement>,
|
||||
{ x, y }: { x: number; y: number },
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
fullShape?: boolean,
|
||||
): boolean => {
|
||||
|
@ -9,22 +9,29 @@ import {
|
||||
|
||||
import {
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
pointDistance,
|
||||
pointFrom,
|
||||
pointFromArray,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||
|
||||
import { pointsOnBezierCurves } from "points-on-curve";
|
||||
|
||||
import type {
|
||||
Curve,
|
||||
Degrees,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getCurvePathOps } from "./utils";
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { generateRoughOptions } from "./Shape";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
@ -38,6 +45,13 @@ import {
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import { getElementShape } from "./shapes";
|
||||
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import type {
|
||||
@ -45,8 +59,10 @@ import type {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
@ -251,6 +267,199 @@ export const getElementAbsoluteCoords = (
|
||||
];
|
||||
};
|
||||
|
||||
/*
|
||||
* for a given element, `getElementLineSegments` returns line segments
|
||||
* that can be used for visual collision detection (useful for frames)
|
||||
* as opposed to bounding box collision detection
|
||||
*/
|
||||
/**
|
||||
* Given an element, return the line segments that make up the element.
|
||||
*
|
||||
* Uses helpers from /math
|
||||
*/
|
||||
export const getElementLineSegments = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const shape = getElementShape(element, elementsMap);
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||
|
||||
if (shape.type === "polycurve") {
|
||||
const curves = shape.data;
|
||||
const points = curves
|
||||
.map((curve) => pointsOnBezierCurves(curve, 10))
|
||||
.flat();
|
||||
let i = 0;
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
while (i < points.length - 1) {
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointFrom(points[i][0], points[i][1]),
|
||||
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||
),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return segments;
|
||||
} else if (shape.type === "polyline") {
|
||||
return shape.data as LineSegment<GlobalPoint>[];
|
||||
} else if (_isRectanguloidElement(element)) {
|
||||
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||
const cornerSegments: LineSegment<GlobalPoint>[] = corners
|
||||
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||
.flat();
|
||||
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||
return [...rotatedSides, ...cornerSegments];
|
||||
} else if (element.type === "diamond") {
|
||||
const [sides, corners] = deconstructDiamondElement(element);
|
||||
const cornerSegments = corners
|
||||
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||
.flat();
|
||||
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||
|
||||
return [...rotatedSides, ...cornerSegments];
|
||||
} else if (shape.type === "polygon") {
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (container && isLinearElement(container)) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [
|
||||
lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
|
||||
lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
|
||||
lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
|
||||
lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
|
||||
];
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
|
||||
const points = shape.data as GlobalPoint[];
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
segments.push(lineSegment(points[i], points[i + 1]));
|
||||
}
|
||||
return segments;
|
||||
} else if (shape.type === "ellipse") {
|
||||
return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
|
||||
}
|
||||
|
||||
const [nw, ne, sw, se, , , w, e] = (
|
||||
[
|
||||
[x1, y1],
|
||||
[x2, y1],
|
||||
[x1, y2],
|
||||
[x2, y2],
|
||||
[cx, y1],
|
||||
[cx, y2],
|
||||
[x1, cy],
|
||||
[x2, cy],
|
||||
] as GlobalPoint[]
|
||||
).map((point) => pointRotateRads(point, center, element.angle));
|
||||
|
||||
return [
|
||||
lineSegment(nw, ne),
|
||||
lineSegment(sw, se),
|
||||
lineSegment(nw, sw),
|
||||
lineSegment(ne, se),
|
||||
lineSegment(nw, e),
|
||||
lineSegment(sw, e),
|
||||
lineSegment(ne, w),
|
||||
lineSegment(se, w),
|
||||
];
|
||||
};
|
||||
|
||||
const _isRectanguloidElement = (
|
||||
element: ExcalidrawElement,
|
||||
): element is ExcalidrawRectanguloidElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(element.type === "rectangle" ||
|
||||
element.type === "image" ||
|
||||
element.type === "iframe" ||
|
||||
element.type === "embeddable" ||
|
||||
element.type === "frame" ||
|
||||
element.type === "magicframe" ||
|
||||
(element.type === "text" && !element.containerId))
|
||||
);
|
||||
};
|
||||
|
||||
const getRotatedSides = (
|
||||
sides: LineSegment<GlobalPoint>[],
|
||||
center: GlobalPoint,
|
||||
angle: Radians,
|
||||
) => {
|
||||
return sides.map((side) => {
|
||||
return lineSegment(
|
||||
pointRotateRads<GlobalPoint>(side[0], center, angle),
|
||||
pointRotateRads<GlobalPoint>(side[1], center, angle),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getSegmentsOnCurve = (
|
||||
curve: Curve<GlobalPoint>,
|
||||
center: GlobalPoint,
|
||||
angle: Radians,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const points = pointsOnBezierCurves(curve, 10);
|
||||
let i = 0;
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
while (i < points.length - 1) {
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointRotateRads<GlobalPoint>(
|
||||
pointFrom(points[i][0], points[i][1]),
|
||||
center,
|
||||
angle,
|
||||
),
|
||||
pointRotateRads<GlobalPoint>(
|
||||
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||
center,
|
||||
angle,
|
||||
),
|
||||
),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
const getSegmentsOnEllipse = (
|
||||
ellipse: ExcalidrawEllipseElement,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
ellipse.x + ellipse.width / 2,
|
||||
ellipse.y + ellipse.height / 2,
|
||||
);
|
||||
|
||||
const a = ellipse.width / 2;
|
||||
const b = ellipse.height / 2;
|
||||
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
const points: GlobalPoint[] = [];
|
||||
const n = 90;
|
||||
const deltaT = (Math.PI * 2) / n;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = i * deltaT;
|
||||
const x = center[0] + a * Math.cos(t);
|
||||
const y = center[1] + b * Math.sin(t);
|
||||
points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
|
||||
}
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
segments.push(lineSegment(points[i], points[i + 1]));
|
||||
}
|
||||
|
||||
segments.push(lineSegment(points[points.length - 1], points[0]));
|
||||
return segments;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||
*
|
||||
@ -659,7 +868,7 @@ const generateLinearElementShape = (
|
||||
})();
|
||||
|
||||
return generator[method](
|
||||
element.points as LocalPoint[] as RoughPoint[],
|
||||
element.points as Mutable<LocalPoint>[] as RoughPoint[],
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,3 @@
|
||||
import { simplify } from "points-on-curve";
|
||||
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
|
||||
import {
|
||||
isTransparent,
|
||||
elementCenterPoint,
|
||||
@ -21,43 +17,43 @@ import {
|
||||
vectorFromPoint,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
curve,
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveOffsetPoints,
|
||||
pointFromArray,
|
||||
rectangle,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
ellipse,
|
||||
ellipseSegmentInterceptPoints,
|
||||
} from "@excalidraw/math";
|
||||
} from "@excalidraw/math/ellipse";
|
||||
|
||||
import type {
|
||||
Curve,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getCornerRadius, isPathALoop } from "./shapes";
|
||||
import { getDiamondPoints, getElementBounds } from "./bounds";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { distanceToElement } from "./distance";
|
||||
import { generateElbowArrowRougJshPathCommands } from "./utils";
|
||||
|
||||
import { isPathALoop } from "./shapes";
|
||||
import { getElementBounds } from "./bounds";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isElbowArrow,
|
||||
isFreeDrawElement,
|
||||
isIframeLikeElement,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type { Options } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
||||
import { distanceToElement } from "./distance";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
@ -115,9 +111,9 @@ export const hitElementItself = ({
|
||||
? shouldTestInside(element)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInElement(point, element) ||
|
||||
isPointOnElementOutline(point, element, threshold)
|
||||
: isPointOnElementOutline(point, element, threshold)
|
||||
isPointInShape(point, element) ||
|
||||
isPointOnShape(point, element, threshold)
|
||||
: isPointOnShape(point, element, threshold)
|
||||
: false;
|
||||
|
||||
// hit test against a frame's name
|
||||
@ -181,7 +177,7 @@ export const hitElementBoundText = (
|
||||
}
|
||||
: boundTextElementCandidate;
|
||||
|
||||
return isPointInElement(point, boundTextElement);
|
||||
return isPointInShape(point, boundTextElement);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -222,17 +218,19 @@ const intersectLinearOrFreeDrawWithLineSegment = (
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
segment: LineSegment<GlobalPoint>,
|
||||
): GlobalPoint[] => {
|
||||
const shapes = deconstructLinearOrFreeDrawElementForCollision(element);
|
||||
const shapes = deconstructLinearOrFreeDrawElement(element);
|
||||
const intersections: GlobalPoint[] = [];
|
||||
|
||||
for (const shape of shapes) {
|
||||
switch (true) {
|
||||
case isCurve(shape):
|
||||
//debugDrawCubicBezier(shape);
|
||||
intersections.push(
|
||||
...curveIntersectLineSegment(shape as Curve<GlobalPoint>, segment),
|
||||
);
|
||||
continue;
|
||||
case isLineSegment(shape):
|
||||
//debugDrawLine(shape);
|
||||
const point = lineSegmentIntersectionPoints(
|
||||
segment,
|
||||
shape as LineSegment<GlobalPoint>,
|
||||
@ -269,10 +267,7 @@ const intersectRectanguloidWithLineSegment = (
|
||||
);
|
||||
|
||||
// Get the element's building components we can test against
|
||||
const [sides, corners] = deconstructRectanguloidElementForCollision(
|
||||
element,
|
||||
offset,
|
||||
);
|
||||
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
||||
|
||||
return (
|
||||
// Test intersection against the sides, keep only the valid
|
||||
@ -323,10 +318,7 @@ const intersectDiamondWithLineSegment = (
|
||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||
|
||||
const [sides, curves] = deconstructDiamondElementForCollision(
|
||||
element,
|
||||
offset,
|
||||
);
|
||||
const [sides, curves] = deconstructDiamondElement(element, offset);
|
||||
|
||||
return (
|
||||
sides
|
||||
@ -379,14 +371,14 @@ const intersectEllipseWithLineSegment = (
|
||||
};
|
||||
|
||||
// check if the given point is considered on the given shape's border
|
||||
const isPointOnElementOutline = (
|
||||
const isPointOnShape = (
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
tolerance = 1,
|
||||
) => distanceToElement(element, point) <= tolerance;
|
||||
|
||||
// check if the given point is considered inside the element's border
|
||||
export const isPointInElement = (
|
||||
export const isPointInShape = (
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
) => {
|
||||
@ -415,437 +407,3 @@ export const isPointInElement = (
|
||||
|
||||
return intersections.length % 2 === 1;
|
||||
};
|
||||
|
||||
export function deconstructLinearOrFreeDrawElementForCollision(
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
): (Curve<GlobalPoint> | LineSegment<GlobalPoint>)[] {
|
||||
const ops = generateLinearShapesForCollision(element) as {
|
||||
op: string;
|
||||
data: number[];
|
||||
}[];
|
||||
const components = [];
|
||||
|
||||
for (let idx = 0; idx < ops.length; idx += 1) {
|
||||
const op = ops[idx];
|
||||
const prevPoint =
|
||||
ops[idx - 1] && pointFromArray<LocalPoint>(ops[idx - 1].data.slice(-2));
|
||||
switch (op.op) {
|
||||
case "move":
|
||||
continue;
|
||||
case "lineTo":
|
||||
if (!prevPoint) {
|
||||
throw new Error("prevPoint is undefined");
|
||||
}
|
||||
|
||||
components.push(
|
||||
lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + prevPoint[0],
|
||||
element.y + prevPoint[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
case "bcurveTo":
|
||||
if (!prevPoint) {
|
||||
throw new Error("prevPoint is undefined");
|
||||
}
|
||||
|
||||
components.push(
|
||||
curve<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + prevPoint[0],
|
||||
element.y + prevPoint[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[2],
|
||||
element.y + op.data[3],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[4],
|
||||
element.y + op.data[5],
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
default: {
|
||||
console.error("Unknown op type", op.op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the building components of a rectanguloid element in the form of
|
||||
* line segments and curves.
|
||||
*
|
||||
* @param element Target rectanguloid element
|
||||
* @param offset Optional offset to expand the rectanguloid shape
|
||||
* @returns Tuple of line segments (0) and curves (1)
|
||||
*/
|
||||
export function deconstructRectanguloidElementForCollision(
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
let radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
|
||||
if (radius === 0) {
|
||||
radius = 0.01;
|
||||
}
|
||||
|
||||
const r = rectangle(
|
||||
pointFrom(element.x, element.y),
|
||||
pointFrom(element.x + element.width, element.y + element.height),
|
||||
);
|
||||
|
||||
const top = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + radius, r[0][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - radius, r[0][1]),
|
||||
);
|
||||
const right = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + radius),
|
||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - radius),
|
||||
);
|
||||
const bottom = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + radius, r[1][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - radius, r[1][1]),
|
||||
);
|
||||
const left = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - radius),
|
||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + radius),
|
||||
);
|
||||
|
||||
const baseCorners = [
|
||||
curve(
|
||||
left[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
||||
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
||||
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
||||
),
|
||||
top[0],
|
||||
), // TOP LEFT
|
||||
curve(
|
||||
top[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
||||
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
||||
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
||||
),
|
||||
right[0],
|
||||
), // TOP RIGHT
|
||||
curve(
|
||||
right[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
||||
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
||||
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
||||
),
|
||||
bottom[1],
|
||||
), // BOTTOM RIGHT
|
||||
curve(
|
||||
bottom[0],
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
||||
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
||||
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
||||
),
|
||||
left[0],
|
||||
), // BOTTOM LEFT
|
||||
];
|
||||
|
||||
const corners =
|
||||
offset > 0
|
||||
? baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(
|
||||
curveOffsetPoints(corner, offset),
|
||||
)!,
|
||||
)
|
||||
: [
|
||||
[baseCorners[0]],
|
||||
[baseCorners[1]],
|
||||
[baseCorners[2]],
|
||||
[baseCorners[3]],
|
||||
];
|
||||
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[0][corners[0].length - 1][3],
|
||||
corners[1][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[1][corners[1].length - 1][3],
|
||||
corners[2][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[2][corners[2].length - 1][3],
|
||||
corners[3][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[3][corners[3].length - 1][3],
|
||||
corners[0][0][0],
|
||||
),
|
||||
];
|
||||
|
||||
return [sides, corners.flat()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the building components of a diamond element in the form of
|
||||
* line segments and curves as a tuple, in this order.
|
||||
*
|
||||
* @param element The element to deconstruct
|
||||
* @param offset An optional offset
|
||||
* @returns Tuple of line segments (0) and curves (1)
|
||||
*/
|
||||
export function deconstructDiamondElementForCollision(
|
||||
element: ExcalidrawDiamondElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
|
||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||
pointFrom(element.x + topX, element.y + topY),
|
||||
pointFrom(element.x + rightX, element.y + rightY),
|
||||
pointFrom(element.x + bottomX, element.y + bottomY),
|
||||
pointFrom(element.x + leftX, element.y + leftY),
|
||||
];
|
||||
|
||||
const baseCorners = [
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] - horizontalRadius,
|
||||
),
|
||||
right,
|
||||
right,
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] + horizontalRadius,
|
||||
),
|
||||
), // RIGHT
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] + verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
bottom,
|
||||
bottom,
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] - verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
), // BOTTOM
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] + horizontalRadius,
|
||||
),
|
||||
left,
|
||||
left,
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] - horizontalRadius,
|
||||
),
|
||||
), // LEFT
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] - verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
top,
|
||||
top,
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] + verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
), // TOP
|
||||
];
|
||||
|
||||
const corners =
|
||||
offset > 0
|
||||
? baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(
|
||||
curveOffsetPoints(corner, offset),
|
||||
)!,
|
||||
)
|
||||
: [
|
||||
[baseCorners[0]],
|
||||
[baseCorners[1]],
|
||||
[baseCorners[2]],
|
||||
[baseCorners[3]],
|
||||
];
|
||||
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[0][corners[0].length - 1][3],
|
||||
corners[1][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[1][corners[1].length - 1][3],
|
||||
corners[2][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[2][corners[2].length - 1][3],
|
||||
corners[3][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[3][corners[3].length - 1][3],
|
||||
corners[0][0][0],
|
||||
),
|
||||
];
|
||||
|
||||
return [sides, corners.flat()];
|
||||
}
|
||||
|
||||
const generateLinearShapesForCollision = (
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
) => {
|
||||
const generator = new RoughGenerator();
|
||||
const options: Options = {
|
||||
seed: element.seed,
|
||||
disableMultiStroke: true,
|
||||
disableMultiStrokeFill: true,
|
||||
roughness: 0,
|
||||
preserveVertices: true,
|
||||
};
|
||||
|
||||
switch (element.type) {
|
||||
case "line":
|
||||
case "arrow": {
|
||||
// points array can be empty in the beginning, so it is important to add
|
||||
// initial position to it
|
||||
const points = element.points.length
|
||||
? element.points
|
||||
: [pointFrom<LocalPoint>(0, 0)];
|
||||
const [x1, y1, x2, y2] = getElementBounds(
|
||||
{
|
||||
...element,
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
new Map(),
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
return generator.path(
|
||||
generateElbowArrowRougJshPathCommands(points, 16),
|
||||
options,
|
||||
).sets[0].ops;
|
||||
} else if (!element.roundness) {
|
||||
return points.map((point, idx) => {
|
||||
const p = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x + point[0], element.y + point[1]),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
return {
|
||||
op: idx === 0 ? "move" : "lineTo",
|
||||
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return generator
|
||||
.curve(points as unknown as RoughPoint[], options)
|
||||
.sets[0].ops.slice(0, element.points.length)
|
||||
.map((op, i, arr) => {
|
||||
if (i === 0) {
|
||||
const p = pointRotateRads<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
return {
|
||||
op: "move",
|
||||
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
op: "bcurveTo",
|
||||
data: [
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[2],
|
||||
element.y + op.data[3],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[4],
|
||||
element.y + op.data[5],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
]
|
||||
.map((p) =>
|
||||
pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
)
|
||||
.flat(),
|
||||
};
|
||||
});
|
||||
}
|
||||
case "freedraw": {
|
||||
if (element.points.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const simplifiedPoints = simplify(element.points as LocalPoint[], 0.75);
|
||||
|
||||
return generator
|
||||
.curve(simplifiedPoints as [number, number][], options)
|
||||
.sets[0].ops.slice(0, element.points.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -18,10 +18,10 @@ import type {
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
deconstructDiamondElementForCollision,
|
||||
deconstructLinearOrFreeDrawElementForCollision,
|
||||
deconstructRectanguloidElementForCollision,
|
||||
} from "./collision";
|
||||
deconstructDiamondElement,
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type {
|
||||
ExcalidrawDiamondElement,
|
||||
@ -75,7 +75,7 @@ const distanceToRectanguloidElement = (
|
||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
||||
// Get the element's building components we can test against
|
||||
const [sides, corners] = deconstructRectanguloidElementForCollision(element);
|
||||
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||
|
||||
return Math.min(
|
||||
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
||||
@ -103,7 +103,7 @@ const distanceToDiamondElement = (
|
||||
// points. It's all the same distance-wise.
|
||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
||||
const [sides, curves] = deconstructDiamondElementForCollision(element);
|
||||
const [sides, curves] = deconstructDiamondElement(element);
|
||||
|
||||
return Math.min(
|
||||
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
||||
@ -137,7 +137,7 @@ const distanceToLinearOrFreeDraElement = (
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
p: GlobalPoint,
|
||||
) => {
|
||||
const shapes = deconstructLinearOrFreeDrawElementForCollision(element);
|
||||
const shapes = deconstructLinearOrFreeDrawElement(element);
|
||||
let distance = Infinity;
|
||||
|
||||
for (const shape of shapes) {
|
||||
|
@ -15,7 +15,7 @@ import { getElementsWithinSelection, getSelectedElements } from "./selection";
|
||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||
|
||||
import {
|
||||
approximateElementWithLineSegments,
|
||||
getElementLineSegments,
|
||||
getCommonBounds,
|
||||
getElementAbsoluteCoords,
|
||||
} from "./bounds";
|
||||
@ -69,15 +69,9 @@ export function isElementIntersectingFrame(
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
const frameLineSegments = approximateElementWithLineSegments(
|
||||
frame,
|
||||
elementsMap,
|
||||
);
|
||||
const frameLineSegments = getElementLineSegments(frame, elementsMap);
|
||||
|
||||
const elementLineSegments = approximateElementWithLineSegments(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
const elementLineSegments = getElementLineSegments(element, elementsMap);
|
||||
|
||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||
elementLineSegments.some((elementLineSegment) =>
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
vectorFromPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||
|
||||
import {
|
||||
DRAGGING_THRESHOLD,
|
||||
KEYS,
|
||||
@ -18,7 +20,7 @@ import {
|
||||
tupleToCoors,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { getCurvePathOps, type Store } from "@excalidraw/element";
|
||||
import type { Store } from "@excalidraw/element";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
|
@ -16,21 +16,116 @@ import {
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
} from "@excalidraw/math";
|
||||
import {
|
||||
getClosedCurveShape,
|
||||
getCurvePathOps,
|
||||
getCurveShape,
|
||||
getEllipseShape,
|
||||
getFreedrawShape,
|
||||
getPolygonShape,
|
||||
type GeometricShape,
|
||||
} from "@excalidraw/utils/shape";
|
||||
|
||||
import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { shouldTestInside } from "./collision";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
|
||||
import { getCurvePathOps } from "./utils";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import { getElementAbsoluteCoords, type Bounds } from "./bounds";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* get the pure geometric shape of an excalidraw elementw
|
||||
* which is then used for hit detection
|
||||
*/
|
||||
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): GeometricShape<Point> => {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "diamond":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
case "embeddable":
|
||||
case "image":
|
||||
case "iframe":
|
||||
case "text":
|
||||
case "selection":
|
||||
return getPolygonShape(element);
|
||||
case "arrow":
|
||||
case "line": {
|
||||
const roughShape =
|
||||
ShapeCache.get(element)?.[0] ??
|
||||
ShapeCache.generateElementShape(element, null)[0];
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||
|
||||
return shouldTestInside(element)
|
||||
? getClosedCurveShape<Point>(
|
||||
element,
|
||||
roughShape,
|
||||
pointFrom<Point>(element.x, element.y),
|
||||
element.angle,
|
||||
pointFrom(cx, cy),
|
||||
)
|
||||
: getCurveShape<Point>(
|
||||
roughShape,
|
||||
pointFrom<Point>(element.x, element.y),
|
||||
element.angle,
|
||||
pointFrom(cx, cy),
|
||||
);
|
||||
}
|
||||
|
||||
case "ellipse":
|
||||
return getEllipseShape(element);
|
||||
|
||||
case "freedraw": {
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||
return getFreedrawShape(
|
||||
element,
|
||||
pointFrom(cx, cy),
|
||||
shouldTestInside(element),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getBoundTextShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): GeometricShape<Point> | null => {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (boundTextElement) {
|
||||
if (element.type === "arrow") {
|
||||
return getElementShape(
|
||||
{
|
||||
...boundTextElement,
|
||||
// arrow's bound text accurate position is not stored in the element's property
|
||||
// but rather calculated and returned from the following static method
|
||||
...LinearElementEditor.getBoundTextElementPosition(
|
||||
element,
|
||||
boundTextElement,
|
||||
elementsMap,
|
||||
),
|
||||
},
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
return getElementShape(boundTextElement, elementsMap);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getControlPointsForBezierCurve = <
|
||||
P extends GlobalPoint | LocalPoint,
|
||||
>(
|
||||
|
@ -1,81 +1,341 @@
|
||||
import { pointDistance } from "@excalidraw/math";
|
||||
import {
|
||||
curve,
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveOffsetPoints,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
pointFromArray,
|
||||
rectangle,
|
||||
type GlobalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { headingForPointIsHorizontal } from "./heading";
|
||||
import { getCornerRadius } from "./shapes";
|
||||
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import { getDiamondPoints } from "./bounds";
|
||||
|
||||
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||
for (const set of shape.sets) {
|
||||
if (set.type === "path") {
|
||||
return set.ops;
|
||||
}
|
||||
}
|
||||
return shape.sets[0].ops;
|
||||
};
|
||||
import { generateLinearCollisionShape } from "./Shape";
|
||||
|
||||
export const generateElbowArrowRougJshPathCommands = (
|
||||
points: readonly LocalPoint[],
|
||||
radius: number,
|
||||
) => {
|
||||
const subpoints = [] as [number, number][];
|
||||
for (let i = 1; i < points.length - 1; i += 1) {
|
||||
const prev = points[i - 1];
|
||||
const next = points[i + 1];
|
||||
const point = points[i];
|
||||
const prevIsHorizontal = headingForPointIsHorizontal(point, prev);
|
||||
const nextIsHorizontal = headingForPointIsHorizontal(next, point);
|
||||
const corner = Math.min(
|
||||
radius,
|
||||
pointDistance(points[i], next) / 2,
|
||||
pointDistance(points[i], prev) / 2,
|
||||
);
|
||||
import type {
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
|
||||
if (prevIsHorizontal) {
|
||||
if (prev[0] < point[0]) {
|
||||
// LEFT
|
||||
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||
} else {
|
||||
// RIGHT
|
||||
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||
export function deconstructLinearOrFreeDrawElement(
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
): (Curve<GlobalPoint> | LineSegment<GlobalPoint>)[] {
|
||||
const ops = generateLinearCollisionShape(element) as {
|
||||
op: string;
|
||||
data: number[];
|
||||
}[];
|
||||
const components = [];
|
||||
|
||||
for (let idx = 0; idx < ops.length; idx += 1) {
|
||||
const op = ops[idx];
|
||||
const prevPoint =
|
||||
ops[idx - 1] && pointFromArray<LocalPoint>(ops[idx - 1].data.slice(-2));
|
||||
switch (op.op) {
|
||||
case "move":
|
||||
continue;
|
||||
case "lineTo":
|
||||
if (!prevPoint) {
|
||||
throw new Error("prevPoint is undefined");
|
||||
}
|
||||
|
||||
components.push(
|
||||
lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + prevPoint[0],
|
||||
element.y + prevPoint[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
case "bcurveTo":
|
||||
if (!prevPoint) {
|
||||
throw new Error("prevPoint is undefined");
|
||||
}
|
||||
|
||||
components.push(
|
||||
curve<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + prevPoint[0],
|
||||
element.y + prevPoint[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[2],
|
||||
element.y + op.data[3],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[4],
|
||||
element.y + op.data[5],
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
default: {
|
||||
console.error("Unknown op type", op.op);
|
||||
}
|
||||
} else if (prev[1] < point[1]) {
|
||||
// UP
|
||||
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||
} else {
|
||||
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||
}
|
||||
|
||||
subpoints.push(points[i] as [number, number]);
|
||||
|
||||
if (nextIsHorizontal) {
|
||||
if (next[0] < point[0]) {
|
||||
// LEFT
|
||||
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||
} else {
|
||||
// RIGHT
|
||||
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||
}
|
||||
} else if (next[1] < point[1]) {
|
||||
// UP
|
||||
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||
} else {
|
||||
// DOWN
|
||||
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||
}
|
||||
}
|
||||
|
||||
const d = [`M ${points[0][0]} ${points[0][1]}`];
|
||||
for (let i = 0; i < subpoints.length; i += 3) {
|
||||
d.push(`L ${subpoints[i][0]} ${subpoints[i][1]}`);
|
||||
d.push(
|
||||
`Q ${subpoints[i + 1][0]} ${subpoints[i + 1][1]}, ${
|
||||
subpoints[i + 2][0]
|
||||
} ${subpoints[i + 2][1]}`,
|
||||
);
|
||||
}
|
||||
d.push(`L ${points[points.length - 1][0]} ${points[points.length - 1][1]}`);
|
||||
return components;
|
||||
}
|
||||
|
||||
return d.join(" ");
|
||||
};
|
||||
/**
|
||||
* Get the building components of a rectanguloid element in the form of
|
||||
* line segments and curves.
|
||||
*
|
||||
* @param element Target rectanguloid element
|
||||
* @param offset Optional offset to expand the rectanguloid shape
|
||||
* @returns Tuple of line segments (0) and curves (1)
|
||||
*/
|
||||
export function deconstructRectanguloidElement(
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
let radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
|
||||
if (radius === 0) {
|
||||
radius = 0.01;
|
||||
}
|
||||
|
||||
const r = rectangle(
|
||||
pointFrom(element.x, element.y),
|
||||
pointFrom(element.x + element.width, element.y + element.height),
|
||||
);
|
||||
|
||||
const top = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + radius, r[0][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - radius, r[0][1]),
|
||||
);
|
||||
const right = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + radius),
|
||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - radius),
|
||||
);
|
||||
const bottom = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + radius, r[1][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - radius, r[1][1]),
|
||||
);
|
||||
const left = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - radius),
|
||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + radius),
|
||||
);
|
||||
|
||||
const baseCorners = [
|
||||
curve(
|
||||
left[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
||||
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
||||
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
||||
),
|
||||
top[0],
|
||||
), // TOP LEFT
|
||||
curve(
|
||||
top[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
||||
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
||||
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
||||
),
|
||||
right[0],
|
||||
), // TOP RIGHT
|
||||
curve(
|
||||
right[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
||||
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
||||
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
||||
),
|
||||
bottom[1],
|
||||
), // BOTTOM RIGHT
|
||||
curve(
|
||||
bottom[0],
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
||||
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
||||
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
||||
),
|
||||
left[0],
|
||||
), // BOTTOM LEFT
|
||||
];
|
||||
|
||||
const corners =
|
||||
offset > 0
|
||||
? baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(
|
||||
curveOffsetPoints(corner, offset),
|
||||
)!,
|
||||
)
|
||||
: [
|
||||
[baseCorners[0]],
|
||||
[baseCorners[1]],
|
||||
[baseCorners[2]],
|
||||
[baseCorners[3]],
|
||||
];
|
||||
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[0][corners[0].length - 1][3],
|
||||
corners[1][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[1][corners[1].length - 1][3],
|
||||
corners[2][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[2][corners[2].length - 1][3],
|
||||
corners[3][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[3][corners[3].length - 1][3],
|
||||
corners[0][0][0],
|
||||
),
|
||||
];
|
||||
|
||||
return [sides, corners.flat()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the building components of a diamond element in the form of
|
||||
* line segments and curves as a tuple, in this order.
|
||||
*
|
||||
* @param element The element to deconstruct
|
||||
* @param offset An optional offset
|
||||
* @returns Tuple of line segments (0) and curves (1)
|
||||
*/
|
||||
export function deconstructDiamondElement(
|
||||
element: ExcalidrawDiamondElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
|
||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||
pointFrom(element.x + topX, element.y + topY),
|
||||
pointFrom(element.x + rightX, element.y + rightY),
|
||||
pointFrom(element.x + bottomX, element.y + bottomY),
|
||||
pointFrom(element.x + leftX, element.y + leftY),
|
||||
];
|
||||
|
||||
const baseCorners = [
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] - horizontalRadius,
|
||||
),
|
||||
right,
|
||||
right,
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] + horizontalRadius,
|
||||
),
|
||||
), // RIGHT
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] + verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
bottom,
|
||||
bottom,
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] - verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
), // BOTTOM
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] + horizontalRadius,
|
||||
),
|
||||
left,
|
||||
left,
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] - horizontalRadius,
|
||||
),
|
||||
), // LEFT
|
||||
curve(
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] - verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
top,
|
||||
top,
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] + verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
), // TOP
|
||||
];
|
||||
|
||||
const corners =
|
||||
offset > 0
|
||||
? baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(
|
||||
curveOffsetPoints(corner, offset),
|
||||
)!,
|
||||
)
|
||||
: [
|
||||
[baseCorners[0]],
|
||||
[baseCorners[1]],
|
||||
[baseCorners[2]],
|
||||
[baseCorners[3]],
|
||||
];
|
||||
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[0][corners[0].length - 1][3],
|
||||
corners[1][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[1][corners[1].length - 1][3],
|
||||
corners[2][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[2][corners[2].length - 1][3],
|
||||
corners[3][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[3][corners[3].length - 1][3],
|
||||
corners[0][0][0],
|
||||
),
|
||||
];
|
||||
|
||||
return [sides, corners.flat()];
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ import {
|
||||
refreshTextDimensions,
|
||||
deepCopyElement,
|
||||
duplicateElements,
|
||||
isPointInElement,
|
||||
isPointInShape,
|
||||
hasBoundTextElement,
|
||||
isArrowElement,
|
||||
isBindingElement,
|
||||
@ -5164,7 +5164,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
// if hitting the bounding box, return early
|
||||
// but if not, we should check for other cases as well (e.g. frame name)
|
||||
if (isPointInElement(pointFrom(x, y), element)) {
|
||||
if (isPointInShape(pointFrom(x, y), element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||
import { getElementLineSegments, isPointInShape } from "@excalidraw/element";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
intersectElementWithLineSegment,
|
||||
isPointInElement,
|
||||
} from "@excalidraw/element";
|
||||
import { lineSegment, pointFrom } from "@excalidraw/math";
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
pointFrom,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getElementsInGroup } from "@excalidraw/element";
|
||||
|
||||
@ -12,7 +12,12 @@ import { shouldTestInside } from "@excalidraw/element";
|
||||
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
||||
import { getBoundTextElementId } from "@excalidraw/element";
|
||||
|
||||
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||
import type {
|
||||
ElementsSegmentsMap,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
} from "@excalidraw/math/types";
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { AnimatedTrail } from "../animated-trail";
|
||||
@ -28,6 +33,8 @@ export class EraserTrail extends AnimatedTrail {
|
||||
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
||||
private geometricShapesCache: Map<string, GeometricShape<GlobalPoint>> =
|
||||
new Map();
|
||||
|
||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||
super(animationFrameHandler, app, {
|
||||
@ -103,6 +110,8 @@ export class EraserTrail extends AnimatedTrail {
|
||||
const intersects = eraserTest(
|
||||
pathSegments,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
@ -139,6 +148,8 @@ export class EraserTrail extends AnimatedTrail {
|
||||
const intersects = eraserTest(
|
||||
pathSegments,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
@ -190,23 +201,31 @@ export class EraserTrail extends AnimatedTrail {
|
||||
const eraserTest = (
|
||||
pathSegments: LineSegment<GlobalPoint>[],
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
||||
elementsMap: ElementsMap,
|
||||
app: App,
|
||||
): boolean => {
|
||||
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||
if (shouldTestInside(element) && isPointInElement(lastPoint, element)) {
|
||||
if (shouldTestInside(element) && isPointInShape(lastPoint, element)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const offset = app.getElementHitThreshold();
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
let elementSegments = elementsSegments.get(element.id);
|
||||
|
||||
return pathSegments.some(
|
||||
(pathSegment) =>
|
||||
intersectElementWithLineSegment(element, pathSegment, offset).length >
|
||||
0 ||
|
||||
(boundTextElement &&
|
||||
intersectElementWithLineSegment(boundTextElement, pathSegment, offset)
|
||||
.length > 0),
|
||||
if (!elementSegments) {
|
||||
elementSegments = getElementLineSegments(element, elementsMap);
|
||||
elementsSegments.set(element.id, elementSegments);
|
||||
}
|
||||
|
||||
return pathSegments.some((pathSegment) =>
|
||||
elementSegments?.some(
|
||||
(elementSegment) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
pathSegment,
|
||||
elementSegment,
|
||||
app.getElementHitThreshold(),
|
||||
) !== null,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
pointFrom,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { approximateElementWithLineSegments } from "@excalidraw/element";
|
||||
import { getElementLineSegments } from "@excalidraw/element";
|
||||
import { LinearElementEditor } from "@excalidraw/element";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
@ -190,10 +190,7 @@ export class LassoTrail extends AnimatedTrail {
|
||||
this.elementsSegments = new Map();
|
||||
const visibleElementsMap = arrayToMap(this.app.visibleElements);
|
||||
for (const element of this.app.visibleElements) {
|
||||
const segments = approximateElementWithLineSegments(
|
||||
element,
|
||||
visibleElementsMap,
|
||||
);
|
||||
const segments = getElementLineSegments(element, visibleElementsMap);
|
||||
this.elementsSegments.set(element.id, segments);
|
||||
}
|
||||
}
|
||||
|
@ -13,12 +13,11 @@ import type {
|
||||
LineSegment,
|
||||
} from "@excalidraw/math/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import { intersectElementWithLineSegment } from "@excalidraw/element";
|
||||
import App from "../components/App";
|
||||
|
||||
export const getLassoSelectedElementIds = (input: {
|
||||
lassoPath: GlobalPoint[];
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elementsSegments: ElementsSegmentsMap;
|
||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||
simplifyDistance?: number;
|
||||
@ -28,6 +27,7 @@ export const getLassoSelectedElementIds = (input: {
|
||||
const {
|
||||
lassoPath,
|
||||
elements,
|
||||
elementsSegments,
|
||||
intersectedElements,
|
||||
enclosedElements,
|
||||
simplifyDistance,
|
||||
@ -48,7 +48,7 @@ export const getLassoSelectedElementIds = (input: {
|
||||
if (enclosed) {
|
||||
enclosedElements.add(element.id);
|
||||
} else {
|
||||
const intersects = intersectionTest(path, element, app);
|
||||
const intersects = intersectionTest(path, element, elementsSegments);
|
||||
if (intersects) {
|
||||
intersectedElements.add(element.id);
|
||||
}
|
||||
@ -66,13 +66,13 @@ export const getLassoSelectedElementIds = (input: {
|
||||
const enclosureTest = (
|
||||
lassoPath: GlobalPoint[],
|
||||
element: ExcalidrawElement,
|
||||
app: App,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
): boolean => {
|
||||
const lassoSegments = lassoPath
|
||||
.slice(1)
|
||||
.map((point, index) => lineSegment(lassoPath[index], point))
|
||||
.concat(lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0]));
|
||||
const offset = app.getElementHitThreshold();
|
||||
const lassoPolygon = polygonFromPoints(lassoPath);
|
||||
const segments = elementsSegments.get(element.id);
|
||||
if (!segments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return segments.some((segment) => {
|
||||
return segment.some((point) =>
|
||||
@ -84,15 +84,26 @@ const enclosureTest = (
|
||||
const intersectionTest = (
|
||||
lassoPath: GlobalPoint[],
|
||||
element: ExcalidrawElement,
|
||||
app: App,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
): boolean => {
|
||||
const lassoSegments = lassoPath
|
||||
.slice(1)
|
||||
.map((point, index) => lineSegment(lassoPath[index], point))
|
||||
.concat(lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0]));
|
||||
const offset = app.getElementHitThreshold();
|
||||
const elementSegments = elementsSegments.get(element.id);
|
||||
if (!elementSegments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
||||
if (index === 0) {
|
||||
return acc;
|
||||
}
|
||||
acc.push(lineSegment(lassoPath[index - 1], point));
|
||||
return acc;
|
||||
}, [] as LineSegment<GlobalPoint>[]);
|
||||
|
||||
return lassoSegments.some((lassoSegment) =>
|
||||
intersectElementWithLineSegment(element, lassoSegment, offset),
|
||||
elementSegments.some(
|
||||
(elementSegment) =>
|
||||
// introduce a bit of tolerance to account for roughness and simplification of paths
|
||||
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
type ElementsSegmentsMap,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { approximateElementWithLineSegments } from "@excalidraw/element";
|
||||
import { getElementLineSegments } from "@excalidraw/element";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
@ -56,7 +56,7 @@ const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => {
|
||||
|
||||
const elementsSegments: ElementsSegmentsMap = new Map();
|
||||
for (const element of h.elements) {
|
||||
const segments = approximateElementWithLineSegments(
|
||||
const segments = getElementLineSegments(
|
||||
element,
|
||||
h.app.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
export * from "./angle";
|
||||
export * from "./curve";
|
||||
export * from "./ellipse";
|
||||
export * from "./line";
|
||||
export * from "./point";
|
||||
export * from "./polygon";
|
||||
|
@ -13,516 +13,532 @@
|
||||
*/
|
||||
import { pointsOnBezierCurves } from "points-on-curve";
|
||||
|
||||
// import { invariant } from "@excalidraw/common";
|
||||
// import {
|
||||
// curve,
|
||||
// lineSegment,
|
||||
// pointFrom,
|
||||
// pointDistance,
|
||||
// pointFromArray,
|
||||
// pointFromVector,
|
||||
// pointRotateRads,
|
||||
// polygon,
|
||||
// polygonFromPoints,
|
||||
// PRECISION,
|
||||
// segmentsIntersectAt,
|
||||
// vector,
|
||||
// vectorAdd,
|
||||
// vectorFromPoint,
|
||||
// vectorScale,
|
||||
// type GlobalPoint,
|
||||
// type LocalPoint,
|
||||
// } from "@excalidraw/math";
|
||||
import { invariant } from "@excalidraw/common";
|
||||
import {
|
||||
curve,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
pointFromArray,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
polygon,
|
||||
polygonFromPoints,
|
||||
PRECISION,
|
||||
segmentsIntersectAt,
|
||||
vector,
|
||||
vectorAdd,
|
||||
vectorFromPoint,
|
||||
vectorScale,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
// import { getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
import { getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
|
||||
// import type {
|
||||
// ElementsMap,
|
||||
// ExcalidrawBindableElement,
|
||||
// ExcalidrawDiamondElement,
|
||||
// ExcalidrawElement,
|
||||
// ExcalidrawEllipseElement,
|
||||
// ExcalidrawEmbeddableElement,
|
||||
// ExcalidrawFrameLikeElement,
|
||||
// ExcalidrawFreeDrawElement,
|
||||
// ExcalidrawIframeElement,
|
||||
// ExcalidrawImageElement,
|
||||
// ExcalidrawLinearElement,
|
||||
// ExcalidrawRectangleElement,
|
||||
// ExcalidrawSelectionElement,
|
||||
// ExcalidrawTextElement,
|
||||
// } from "@excalidraw/element/types";
|
||||
// import type { Curve, LineSegment, Polygon, Radians } from "@excalidraw/math";
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawIframeElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawTextElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type { Curve, LineSegment, Polygon, Radians } from "@excalidraw/math";
|
||||
|
||||
// // a polyline (made up term here) is a line consisting of other line segments
|
||||
// // this corresponds to a straight line element in the editor but it could also
|
||||
// // be used to model other elements
|
||||
// export type Polyline<Point extends GlobalPoint | LocalPoint> =
|
||||
// LineSegment<Point>[];
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
|
||||
// // a polycurve is a curve consisting of ther curves, this corresponds to a complex
|
||||
// // curve on the canvas
|
||||
// export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
|
||||
// a polyline (made up term here) is a line consisting of other line segments
|
||||
// this corresponds to a straight line element in the editor but it could also
|
||||
// be used to model other elements
|
||||
export type Polyline<Point extends GlobalPoint | LocalPoint> =
|
||||
LineSegment<Point>[];
|
||||
|
||||
// // an ellipse is specified by its center, angle, and its major and minor axes
|
||||
// // but for the sake of simplicity, we've used halfWidth and halfHeight instead
|
||||
// // in replace of semi major and semi minor axes
|
||||
// export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||
// center: Point;
|
||||
// angle: Radians;
|
||||
// halfWidth: number;
|
||||
// halfHeight: number;
|
||||
// };
|
||||
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
|
||||
// curve on the canvas
|
||||
export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
|
||||
|
||||
// export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
||||
// | {
|
||||
// type: "line";
|
||||
// data: LineSegment<Point>;
|
||||
// }
|
||||
// | {
|
||||
// type: "polygon";
|
||||
// data: Polygon<Point>;
|
||||
// }
|
||||
// | {
|
||||
// type: "curve";
|
||||
// data: Curve<Point>;
|
||||
// }
|
||||
// | {
|
||||
// type: "ellipse";
|
||||
// data: Ellipse<Point>;
|
||||
// }
|
||||
// | {
|
||||
// type: "polyline";
|
||||
// data: Polyline<Point>;
|
||||
// }
|
||||
// | {
|
||||
// type: "polycurve";
|
||||
// data: Polycurve<Point>;
|
||||
// };
|
||||
// an ellipse is specified by its center, angle, and its major and minor axes
|
||||
// but for the sake of simplicity, we've used halfWidth and halfHeight instead
|
||||
// in replace of semi major and semi minor axes
|
||||
export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||
center: Point;
|
||||
angle: Radians;
|
||||
halfWidth: number;
|
||||
halfHeight: number;
|
||||
};
|
||||
|
||||
// type RectangularElement =
|
||||
// | ExcalidrawRectangleElement
|
||||
// | ExcalidrawDiamondElement
|
||||
// | ExcalidrawFrameLikeElement
|
||||
// | ExcalidrawEmbeddableElement
|
||||
// | ExcalidrawImageElement
|
||||
// | ExcalidrawIframeElement
|
||||
// | ExcalidrawTextElement
|
||||
// | ExcalidrawSelectionElement;
|
||||
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
||||
| {
|
||||
type: "line";
|
||||
data: LineSegment<Point>;
|
||||
}
|
||||
| {
|
||||
type: "polygon";
|
||||
data: Polygon<Point>;
|
||||
}
|
||||
| {
|
||||
type: "curve";
|
||||
data: Curve<Point>;
|
||||
}
|
||||
| {
|
||||
type: "ellipse";
|
||||
data: Ellipse<Point>;
|
||||
}
|
||||
| {
|
||||
type: "polyline";
|
||||
data: Polyline<Point>;
|
||||
}
|
||||
| {
|
||||
type: "polycurve";
|
||||
data: Polycurve<Point>;
|
||||
};
|
||||
|
||||
// // polygon
|
||||
// export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
// element: RectangularElement,
|
||||
// ): GeometricShape<Point> => {
|
||||
// const { angle, width, height, x, y } = element;
|
||||
type RectangularElement =
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawFrameLikeElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawSelectionElement;
|
||||
|
||||
// const cx = x + width / 2;
|
||||
// const cy = y + height / 2;
|
||||
// polygon
|
||||
export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: RectangularElement,
|
||||
): GeometricShape<Point> => {
|
||||
const { angle, width, height, x, y } = element;
|
||||
|
||||
// const center: Point = pointFrom(cx, cy);
|
||||
const cx = x + width / 2;
|
||||
const cy = y + height / 2;
|
||||
|
||||
// let data: Polygon<Point>;
|
||||
const center: Point = pointFrom(cx, cy);
|
||||
|
||||
// if (element.type === "diamond") {
|
||||
// data = polygon(
|
||||
// pointRotateRads(pointFrom(cx, y), center, angle),
|
||||
// pointRotateRads(pointFrom(x + width, cy), center, angle),
|
||||
// pointRotateRads(pointFrom(cx, y + height), center, angle),
|
||||
// pointRotateRads(pointFrom(x, cy), center, angle),
|
||||
// );
|
||||
// } else {
|
||||
// data = polygon(
|
||||
// pointRotateRads(pointFrom(x, y), center, angle),
|
||||
// pointRotateRads(pointFrom(x + width, y), center, angle),
|
||||
// pointRotateRads(pointFrom(x + width, y + height), center, angle),
|
||||
// pointRotateRads(pointFrom(x, y + height), center, angle),
|
||||
// );
|
||||
// }
|
||||
let data: Polygon<Point>;
|
||||
|
||||
// return {
|
||||
// type: "polygon",
|
||||
// data,
|
||||
// };
|
||||
// };
|
||||
if (element.type === "diamond") {
|
||||
data = polygon(
|
||||
pointRotateRads(pointFrom(cx, y), center, angle),
|
||||
pointRotateRads(pointFrom(x + width, cy), center, angle),
|
||||
pointRotateRads(pointFrom(cx, y + height), center, angle),
|
||||
pointRotateRads(pointFrom(x, cy), center, angle),
|
||||
);
|
||||
} else {
|
||||
data = polygon(
|
||||
pointRotateRads(pointFrom(x, y), center, angle),
|
||||
pointRotateRads(pointFrom(x + width, y), center, angle),
|
||||
pointRotateRads(pointFrom(x + width, y + height), center, angle),
|
||||
pointRotateRads(pointFrom(x, y + height), center, angle),
|
||||
);
|
||||
}
|
||||
|
||||
// // return the selection box for an element, possibly rotated as well
|
||||
// export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
// element: ExcalidrawElement,
|
||||
// elementsMap: ElementsMap,
|
||||
// padding = 10,
|
||||
// ) => {
|
||||
// let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||
// element,
|
||||
// elementsMap,
|
||||
// true,
|
||||
// );
|
||||
return {
|
||||
type: "polygon",
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
// x1 -= padding;
|
||||
// x2 += padding;
|
||||
// y1 -= padding;
|
||||
// y2 += padding;
|
||||
// return the selection box for an element, possibly rotated as well
|
||||
export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
padding = 10,
|
||||
) => {
|
||||
let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
true,
|
||||
);
|
||||
|
||||
// //const angleInDegrees = angleToDegrees(element.angle);
|
||||
// const center = pointFrom(cx, cy);
|
||||
// const topLeft = pointRotateRads(pointFrom(x1, y1), center, element.angle);
|
||||
// const topRight = pointRotateRads(pointFrom(x2, y1), center, element.angle);
|
||||
// const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, element.angle);
|
||||
// const bottomRight = pointRotateRads(pointFrom(x2, y2), center, element.angle);
|
||||
x1 -= padding;
|
||||
x2 += padding;
|
||||
y1 -= padding;
|
||||
y2 += padding;
|
||||
|
||||
// return {
|
||||
// type: "polygon",
|
||||
// data: [topLeft, topRight, bottomRight, bottomLeft],
|
||||
// } as GeometricShape<Point>;
|
||||
// };
|
||||
//const angleInDegrees = angleToDegrees(element.angle);
|
||||
const center = pointFrom(cx, cy);
|
||||
const topLeft = pointRotateRads(pointFrom(x1, y1), center, element.angle);
|
||||
const topRight = pointRotateRads(pointFrom(x2, y1), center, element.angle);
|
||||
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, element.angle);
|
||||
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, element.angle);
|
||||
|
||||
// // ellipse
|
||||
// export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
// element: ExcalidrawEllipseElement,
|
||||
// ): GeometricShape<Point> => {
|
||||
// const { width, height, angle, x, y } = element;
|
||||
return {
|
||||
type: "polygon",
|
||||
data: [topLeft, topRight, bottomRight, bottomLeft],
|
||||
} as GeometricShape<Point>;
|
||||
};
|
||||
|
||||
// return {
|
||||
// type: "ellipse",
|
||||
// data: {
|
||||
// center: pointFrom(x + width / 2, y + height / 2),
|
||||
// angle,
|
||||
// halfWidth: width / 2,
|
||||
// halfHeight: height / 2,
|
||||
// },
|
||||
// };
|
||||
// };
|
||||
// ellipse
|
||||
export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawEllipseElement,
|
||||
): GeometricShape<Point> => {
|
||||
const { width, height, angle, x, y } = element;
|
||||
|
||||
// // linear
|
||||
// export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
// roughShape: Drawable,
|
||||
// startingPoint: Point = pointFrom(0, 0),
|
||||
// angleInRadian: Radians,
|
||||
// center: Point,
|
||||
// ): GeometricShape<Point> => {
|
||||
// const transform = (p: Point): Point =>
|
||||
// pointRotateRads(
|
||||
// pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||
// center,
|
||||
// angleInRadian,
|
||||
// );
|
||||
return {
|
||||
type: "ellipse",
|
||||
data: {
|
||||
center: pointFrom(x + width / 2, y + height / 2),
|
||||
angle,
|
||||
halfWidth: width / 2,
|
||||
halfHeight: height / 2,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// const ops = getCurvePathOps(roughShape);
|
||||
// const polycurve: Polycurve<Point> = [];
|
||||
// let p0 = pointFrom<Point>(0, 0);
|
||||
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||
// NOTE (mtolmacs): Temporary fix for extremely large elements
|
||||
if (!shape) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// for (const op of ops) {
|
||||
// if (op.op === "move") {
|
||||
// const p = pointFromArray<Point>(op.data);
|
||||
// invariant(p != null, "Ops data is not a point");
|
||||
// p0 = transform(p);
|
||||
// }
|
||||
// if (op.op === "bcurveTo") {
|
||||
// const p1 = transform(pointFrom<Point>(op.data[0], op.data[1]));
|
||||
// const p2 = transform(pointFrom<Point>(op.data[2], op.data[3]));
|
||||
// const p3 = transform(pointFrom<Point>(op.data[4], op.data[5]));
|
||||
// polycurve.push(curve<Point>(p0, p1, p2, p3));
|
||||
// p0 = p3;
|
||||
// }
|
||||
// }
|
||||
for (const set of shape.sets) {
|
||||
if (set.type === "path") {
|
||||
return set.ops;
|
||||
}
|
||||
}
|
||||
return shape.sets[0].ops;
|
||||
};
|
||||
|
||||
// return {
|
||||
// type: "polycurve",
|
||||
// data: polycurve,
|
||||
// };
|
||||
// };
|
||||
// linear
|
||||
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
roughShape: Drawable,
|
||||
startingPoint: Point = pointFrom(0, 0),
|
||||
angleInRadian: Radians,
|
||||
center: Point,
|
||||
): GeometricShape<Point> => {
|
||||
const transform = (p: Point): Point =>
|
||||
pointRotateRads(
|
||||
pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||
center,
|
||||
angleInRadian,
|
||||
);
|
||||
|
||||
// const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
|
||||
// points: Point[],
|
||||
// ): Polyline<Point> => {
|
||||
// let previousPoint: Point = points[0];
|
||||
// const polyline: LineSegment<Point>[] = [];
|
||||
const ops = getCurvePathOps(roughShape);
|
||||
const polycurve: Polycurve<Point> = [];
|
||||
let p0 = pointFrom<Point>(0, 0);
|
||||
|
||||
// for (let i = 1; i < points.length; i++) {
|
||||
// const nextPoint = points[i];
|
||||
// polyline.push(lineSegment<Point>(previousPoint, nextPoint));
|
||||
// previousPoint = nextPoint;
|
||||
// }
|
||||
for (const op of ops) {
|
||||
if (op.op === "move") {
|
||||
const p = pointFromArray<Point>(op.data);
|
||||
invariant(p != null, "Ops data is not a point");
|
||||
p0 = transform(p);
|
||||
}
|
||||
if (op.op === "bcurveTo") {
|
||||
const p1 = transform(pointFrom<Point>(op.data[0], op.data[1]));
|
||||
const p2 = transform(pointFrom<Point>(op.data[2], op.data[3]));
|
||||
const p3 = transform(pointFrom<Point>(op.data[4], op.data[5]));
|
||||
polycurve.push(curve<Point>(p0, p1, p2, p3));
|
||||
p0 = p3;
|
||||
}
|
||||
}
|
||||
|
||||
// return polyline;
|
||||
// };
|
||||
return {
|
||||
type: "polycurve",
|
||||
data: polycurve,
|
||||
};
|
||||
};
|
||||
|
||||
// export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
// element: ExcalidrawFreeDrawElement,
|
||||
// center: Point,
|
||||
// isClosed: boolean = false,
|
||||
// ): GeometricShape<Point> => {
|
||||
// const transform = (p: Point) =>
|
||||
// pointRotateRads(
|
||||
// pointFromVector(
|
||||
// vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
|
||||
// ),
|
||||
// center,
|
||||
// element.angle,
|
||||
// );
|
||||
const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
|
||||
points: Point[],
|
||||
): Polyline<Point> => {
|
||||
let previousPoint: Point = points[0];
|
||||
const polyline: LineSegment<Point>[] = [];
|
||||
|
||||
// const polyline = polylineFromPoints(
|
||||
// element.points.map((p) => transform(p as Point)),
|
||||
// );
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const nextPoint = points[i];
|
||||
polyline.push(lineSegment<Point>(previousPoint, nextPoint));
|
||||
previousPoint = nextPoint;
|
||||
}
|
||||
|
||||
// return (
|
||||
// isClosed
|
||||
// ? {
|
||||
// type: "polygon",
|
||||
// data: polygonFromPoints(polyline.flat()),
|
||||
// }
|
||||
// : {
|
||||
// type: "polyline",
|
||||
// data: polyline,
|
||||
// }
|
||||
// ) as GeometricShape<Point>;
|
||||
// };
|
||||
return polyline;
|
||||
};
|
||||
|
||||
// export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
// element: ExcalidrawLinearElement,
|
||||
// roughShape: Drawable,
|
||||
// startingPoint: Point = pointFrom<Point>(0, 0),
|
||||
// angleInRadian: Radians,
|
||||
// center: Point,
|
||||
// ): GeometricShape<Point> => {
|
||||
// const transform = (p: Point) =>
|
||||
// pointRotateRads(
|
||||
// pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||
// center,
|
||||
// angleInRadian,
|
||||
// );
|
||||
export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
center: Point,
|
||||
isClosed: boolean = false,
|
||||
): GeometricShape<Point> => {
|
||||
const transform = (p: Point) =>
|
||||
pointRotateRads(
|
||||
pointFromVector(
|
||||
vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
// if (element.roundness === null) {
|
||||
// return {
|
||||
// type: "polygon",
|
||||
// data: polygonFromPoints(
|
||||
// element.points.map((p) => transform(p as Point)) as Point[],
|
||||
// ),
|
||||
// };
|
||||
// }
|
||||
const polyline = polylineFromPoints(
|
||||
element.points.map((p) => transform(p as Point)),
|
||||
);
|
||||
|
||||
// const ops = getCurvePathOps(roughShape);
|
||||
return (
|
||||
isClosed
|
||||
? {
|
||||
type: "polygon",
|
||||
data: polygonFromPoints(polyline.flat()),
|
||||
}
|
||||
: {
|
||||
type: "polyline",
|
||||
data: polyline,
|
||||
}
|
||||
) as GeometricShape<Point>;
|
||||
};
|
||||
|
||||
// const points: Point[] = [];
|
||||
// let odd = false;
|
||||
// for (const operation of ops) {
|
||||
// if (operation.op === "move") {
|
||||
// odd = !odd;
|
||||
// if (odd) {
|
||||
// points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||
// }
|
||||
// } else if (operation.op === "bcurveTo") {
|
||||
// if (odd) {
|
||||
// points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||
// points.push(pointFrom(operation.data[2], operation.data[3]));
|
||||
// points.push(pointFrom(operation.data[4], operation.data[5]));
|
||||
// }
|
||||
// } else if (operation.op === "lineTo") {
|
||||
// if (odd) {
|
||||
// points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawLinearElement,
|
||||
roughShape: Drawable,
|
||||
startingPoint: Point = pointFrom<Point>(0, 0),
|
||||
angleInRadian: Radians,
|
||||
center: Point,
|
||||
): GeometricShape<Point> => {
|
||||
const transform = (p: Point) =>
|
||||
pointRotateRads(
|
||||
pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||
center,
|
||||
angleInRadian,
|
||||
);
|
||||
|
||||
// const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
|
||||
// transform(p as Point),
|
||||
// ) as Point[];
|
||||
if (element.roundness === null) {
|
||||
return {
|
||||
type: "polygon",
|
||||
data: polygonFromPoints(
|
||||
element.points.map((p) => transform(p as Point)) as Point[],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// return {
|
||||
// type: "polygon",
|
||||
// data: polygonFromPoints<Point>(polygonPoints),
|
||||
// };
|
||||
// };
|
||||
const ops = getCurvePathOps(roughShape);
|
||||
|
||||
// /**
|
||||
// * Determine intersection of a rectangular shaped element and a
|
||||
// * line segment.
|
||||
// *
|
||||
// * @param element The rectangular element to test against
|
||||
// * @param segment The segment intersecting the element
|
||||
// * @param gap Optional value to inflate the shape before testing
|
||||
// * @returns An array of intersections
|
||||
// */
|
||||
// // TODO: Replace with final rounded rectangle code
|
||||
// export const segmentIntersectRectangleElement = <
|
||||
// Point extends LocalPoint | GlobalPoint,
|
||||
// >(
|
||||
// element: ExcalidrawBindableElement,
|
||||
// segment: LineSegment<Point>,
|
||||
// gap: number = 0,
|
||||
// ): Point[] => {
|
||||
// const bounds = [
|
||||
// element.x - gap,
|
||||
// element.y - gap,
|
||||
// element.x + element.width + gap,
|
||||
// element.y + element.height + gap,
|
||||
// ];
|
||||
// const center = pointFrom(
|
||||
// (bounds[0] + bounds[2]) / 2,
|
||||
// (bounds[1] + bounds[3]) / 2,
|
||||
// );
|
||||
const points: Point[] = [];
|
||||
let odd = false;
|
||||
for (const operation of ops) {
|
||||
if (operation.op === "move") {
|
||||
odd = !odd;
|
||||
if (odd) {
|
||||
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||
}
|
||||
} else if (operation.op === "bcurveTo") {
|
||||
if (odd) {
|
||||
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||
points.push(pointFrom(operation.data[2], operation.data[3]));
|
||||
points.push(pointFrom(operation.data[4], operation.data[5]));
|
||||
}
|
||||
} else if (operation.op === "lineTo") {
|
||||
if (odd) {
|
||||
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return [
|
||||
// lineSegment(
|
||||
// pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
|
||||
// pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
|
||||
// ),
|
||||
// lineSegment(
|
||||
// pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
|
||||
// pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
|
||||
// ),
|
||||
// lineSegment(
|
||||
// pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
|
||||
// pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
|
||||
// ),
|
||||
// lineSegment(
|
||||
// pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
|
||||
// pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
|
||||
// ),
|
||||
// ]
|
||||
// .map((s) => segmentsIntersectAt(segment, s))
|
||||
// .filter((i): i is Point => !!i);
|
||||
// };
|
||||
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
|
||||
transform(p as Point),
|
||||
) as Point[];
|
||||
|
||||
// const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
// p: Point,
|
||||
// ellipse: Ellipse<Point>,
|
||||
// ) => {
|
||||
// const { angle, halfWidth, halfHeight, center } = ellipse;
|
||||
// const a = halfWidth;
|
||||
// const b = halfHeight;
|
||||
// const translatedPoint = vectorAdd(
|
||||
// vectorFromPoint(p),
|
||||
// vectorScale(vectorFromPoint(center), -1),
|
||||
// );
|
||||
// const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||
// pointFromVector(translatedPoint),
|
||||
// pointFrom(0, 0),
|
||||
// -angle as Radians,
|
||||
// );
|
||||
return {
|
||||
type: "polygon",
|
||||
data: polygonFromPoints<Point>(polygonPoints),
|
||||
};
|
||||
};
|
||||
|
||||
// const px = Math.abs(rotatedPointX);
|
||||
// const py = Math.abs(rotatedPointY);
|
||||
/**
|
||||
* Determine intersection of a rectangular shaped element and a
|
||||
* line segment.
|
||||
*
|
||||
* @param element The rectangular element to test against
|
||||
* @param segment The segment intersecting the element
|
||||
* @param gap Optional value to inflate the shape before testing
|
||||
* @returns An array of intersections
|
||||
*/
|
||||
// TODO: Replace with final rounded rectangle code
|
||||
export const segmentIntersectRectangleElement = <
|
||||
Point extends LocalPoint | GlobalPoint,
|
||||
>(
|
||||
element: ExcalidrawBindableElement,
|
||||
segment: LineSegment<Point>,
|
||||
gap: number = 0,
|
||||
): Point[] => {
|
||||
const bounds = [
|
||||
element.x - gap,
|
||||
element.y - gap,
|
||||
element.x + element.width + gap,
|
||||
element.y + element.height + gap,
|
||||
];
|
||||
const center = pointFrom(
|
||||
(bounds[0] + bounds[2]) / 2,
|
||||
(bounds[1] + bounds[3]) / 2,
|
||||
);
|
||||
|
||||
// let tx = 0.707;
|
||||
// let ty = 0.707;
|
||||
return [
|
||||
lineSegment(
|
||||
pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
|
||||
pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
|
||||
),
|
||||
lineSegment(
|
||||
pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
|
||||
pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
|
||||
),
|
||||
lineSegment(
|
||||
pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
|
||||
pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
|
||||
),
|
||||
lineSegment(
|
||||
pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
|
||||
pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
|
||||
),
|
||||
]
|
||||
.map((s) => segmentsIntersectAt(segment, s))
|
||||
.filter((i): i is Point => !!i);
|
||||
};
|
||||
|
||||
// for (let i = 0; i < 3; i++) {
|
||||
// const x = a * tx;
|
||||
// const y = b * ty;
|
||||
const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
p: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
const { angle, halfWidth, halfHeight, center } = ellipse;
|
||||
const a = halfWidth;
|
||||
const b = halfHeight;
|
||||
const translatedPoint = vectorAdd(
|
||||
vectorFromPoint(p),
|
||||
vectorScale(vectorFromPoint(center), -1),
|
||||
);
|
||||
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||
pointFromVector(translatedPoint),
|
||||
pointFrom(0, 0),
|
||||
-angle as Radians,
|
||||
);
|
||||
|
||||
// const ex = ((a * a - b * b) * tx ** 3) / a;
|
||||
// const ey = ((b * b - a * a) * ty ** 3) / b;
|
||||
const px = Math.abs(rotatedPointX);
|
||||
const py = Math.abs(rotatedPointY);
|
||||
|
||||
// const rx = x - ex;
|
||||
// const ry = y - ey;
|
||||
let tx = 0.707;
|
||||
let ty = 0.707;
|
||||
|
||||
// const qx = px - ex;
|
||||
// const qy = py - ey;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const x = a * tx;
|
||||
const y = b * ty;
|
||||
|
||||
// const r = Math.hypot(ry, rx);
|
||||
// const q = Math.hypot(qy, qx);
|
||||
const ex = ((a * a - b * b) * tx ** 3) / a;
|
||||
const ey = ((b * b - a * a) * ty ** 3) / b;
|
||||
|
||||
// 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 rx = x - ex;
|
||||
const ry = y - ey;
|
||||
|
||||
// const [minX, minY] = [
|
||||
// a * tx * Math.sign(rotatedPointX),
|
||||
// b * ty * Math.sign(rotatedPointY),
|
||||
// ];
|
||||
const qx = px - ex;
|
||||
const qy = py - ey;
|
||||
|
||||
// return pointDistance(
|
||||
// pointFrom(rotatedPointX, rotatedPointY),
|
||||
// pointFrom(minX, minY),
|
||||
// );
|
||||
// };
|
||||
const r = Math.hypot(ry, rx);
|
||||
const q = Math.hypot(qy, qx);
|
||||
|
||||
// export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
// point: Point,
|
||||
// ellipse: Ellipse<Point>,
|
||||
// threshold = PRECISION,
|
||||
// ) => {
|
||||
// return distanceToEllipse(point, ellipse) <= threshold;
|
||||
// };
|
||||
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;
|
||||
}
|
||||
|
||||
// export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
// p: Point,
|
||||
// ellipse: Ellipse<Point>,
|
||||
// ) => {
|
||||
// const { center, angle, halfWidth, halfHeight } = ellipse;
|
||||
// const translatedPoint = vectorAdd(
|
||||
// vectorFromPoint(p),
|
||||
// vectorScale(vectorFromPoint(center), -1),
|
||||
// );
|
||||
// const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||
// pointFromVector(translatedPoint),
|
||||
// pointFrom(0, 0),
|
||||
// -angle as Radians,
|
||||
// );
|
||||
const [minX, minY] = [
|
||||
a * tx * Math.sign(rotatedPointX),
|
||||
b * ty * Math.sign(rotatedPointY),
|
||||
];
|
||||
|
||||
// return (
|
||||
// (rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
|
||||
// (rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
|
||||
// 1
|
||||
// );
|
||||
// };
|
||||
return pointDistance(
|
||||
pointFrom(rotatedPointX, rotatedPointY),
|
||||
pointFrom(minX, minY),
|
||||
);
|
||||
};
|
||||
|
||||
// export const ellipseAxes = <Point extends LocalPoint | GlobalPoint>(
|
||||
// ellipse: Ellipse<Point>,
|
||||
// ) => {
|
||||
// const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
|
||||
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
threshold = PRECISION,
|
||||
) => {
|
||||
return distanceToEllipse(point, ellipse) <= threshold;
|
||||
};
|
||||
|
||||
// const majorAxis = widthGreaterThanHeight
|
||||
// ? ellipse.halfWidth * 2
|
||||
// : ellipse.halfHeight * 2;
|
||||
// const minorAxis = widthGreaterThanHeight
|
||||
// ? ellipse.halfHeight * 2
|
||||
// : ellipse.halfWidth * 2;
|
||||
export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
|
||||
p: Point,
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
const { center, angle, halfWidth, halfHeight } = ellipse;
|
||||
const translatedPoint = vectorAdd(
|
||||
vectorFromPoint(p),
|
||||
vectorScale(vectorFromPoint(center), -1),
|
||||
);
|
||||
const [rotatedPointX, rotatedPointY] = pointRotateRads(
|
||||
pointFromVector(translatedPoint),
|
||||
pointFrom(0, 0),
|
||||
-angle as Radians,
|
||||
);
|
||||
|
||||
// return {
|
||||
// majorAxis,
|
||||
// minorAxis,
|
||||
// };
|
||||
// };
|
||||
return (
|
||||
(rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
|
||||
(rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
|
||||
1
|
||||
);
|
||||
};
|
||||
|
||||
// export const ellipseFocusToCenter = <Point extends LocalPoint | GlobalPoint>(
|
||||
// ellipse: Ellipse<Point>,
|
||||
// ) => {
|
||||
// const { majorAxis, minorAxis } = ellipseAxes(ellipse);
|
||||
export const ellipseAxes = <Point extends LocalPoint | GlobalPoint>(
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
|
||||
|
||||
// return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
|
||||
// };
|
||||
const majorAxis = widthGreaterThanHeight
|
||||
? ellipse.halfWidth * 2
|
||||
: ellipse.halfHeight * 2;
|
||||
const minorAxis = widthGreaterThanHeight
|
||||
? ellipse.halfHeight * 2
|
||||
: ellipse.halfWidth * 2;
|
||||
|
||||
// export const ellipseExtremes = <Point extends LocalPoint | GlobalPoint>(
|
||||
// ellipse: Ellipse<Point>,
|
||||
// ) => {
|
||||
// const { center, angle } = ellipse;
|
||||
// const { majorAxis, minorAxis } = ellipseAxes(ellipse);
|
||||
return {
|
||||
majorAxis,
|
||||
minorAxis,
|
||||
};
|
||||
};
|
||||
|
||||
// const cos = Math.cos(angle);
|
||||
// const sin = Math.sin(angle);
|
||||
export const ellipseFocusToCenter = <Point extends LocalPoint | GlobalPoint>(
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
|
||||
|
||||
// const sqSum = majorAxis ** 2 + minorAxis ** 2;
|
||||
// const sqDiff = (majorAxis ** 2 - minorAxis ** 2) * Math.cos(2 * angle);
|
||||
return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
|
||||
};
|
||||
|
||||
// const yMax = Math.sqrt((sqSum - sqDiff) / 2);
|
||||
// const xAtYMax =
|
||||
// (yMax * sqSum * sin * cos) /
|
||||
// (majorAxis ** 2 * sin ** 2 + minorAxis ** 2 * cos ** 2);
|
||||
export const ellipseExtremes = <Point extends LocalPoint | GlobalPoint>(
|
||||
ellipse: Ellipse<Point>,
|
||||
) => {
|
||||
const { center, angle } = ellipse;
|
||||
const { majorAxis, minorAxis } = ellipseAxes(ellipse);
|
||||
|
||||
// const xMax = Math.sqrt((sqSum + sqDiff) / 2);
|
||||
// const yAtXMax =
|
||||
// (xMax * sqSum * sin * cos) /
|
||||
// (majorAxis ** 2 * cos ** 2 + minorAxis ** 2 * sin ** 2);
|
||||
// const centerVector = vectorFromPoint(center);
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
|
||||
// return [
|
||||
// vectorAdd(vector(xAtYMax, yMax), centerVector),
|
||||
// vectorAdd(vectorScale(vector(xAtYMax, yMax), -1), centerVector),
|
||||
// vectorAdd(vector(xMax, yAtXMax), centerVector),
|
||||
// vectorAdd(vector(xMax, yAtXMax), centerVector),
|
||||
// ];
|
||||
// };
|
||||
const sqSum = majorAxis ** 2 + minorAxis ** 2;
|
||||
const sqDiff = (majorAxis ** 2 - minorAxis ** 2) * Math.cos(2 * angle);
|
||||
|
||||
const yMax = Math.sqrt((sqSum - sqDiff) / 2);
|
||||
const xAtYMax =
|
||||
(yMax * sqSum * sin * cos) /
|
||||
(majorAxis ** 2 * sin ** 2 + minorAxis ** 2 * cos ** 2);
|
||||
|
||||
const xMax = Math.sqrt((sqSum + sqDiff) / 2);
|
||||
const yAtXMax =
|
||||
(xMax * sqSum * sin * cos) /
|
||||
(majorAxis ** 2 * cos ** 2 + minorAxis ** 2 * sin ** 2);
|
||||
const centerVector = vectorFromPoint(center);
|
||||
|
||||
return [
|
||||
vectorAdd(vector(xAtYMax, yMax), centerVector),
|
||||
vectorAdd(vectorScale(vector(xAtYMax, yMax), -1), centerVector),
|
||||
vectorAdd(vector(xMax, yAtXMax), centerVector),
|
||||
vectorAdd(vector(xMax, yAtXMax), centerVector),
|
||||
];
|
||||
};
|
||||
|
@ -8,7 +8,14 @@ import {
|
||||
segmentsIntersectAt,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { GlobalPoint, LineSegment, Polygon } from "@excalidraw/math";
|
||||
import type {
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
Polygon,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { pointInEllipse, pointOnEllipse, type Ellipse } from "../src/shape";
|
||||
|
||||
describe("point and line", () => {
|
||||
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
|
||||
@ -64,56 +71,56 @@ describe("point and polygon", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// describe("point and ellipse", () => {
|
||||
// const ellipse: Ellipse<GlobalPoint> = {
|
||||
// center: pointFrom(0, 0),
|
||||
// angle: 0 as Radians,
|
||||
// halfWidth: 2,
|
||||
// halfHeight: 1,
|
||||
// };
|
||||
describe("point and ellipse", () => {
|
||||
const ellipse: Ellipse<GlobalPoint> = {
|
||||
center: pointFrom(0, 0),
|
||||
angle: 0 as Radians,
|
||||
halfWidth: 2,
|
||||
halfHeight: 1,
|
||||
};
|
||||
|
||||
// it("point on ellipse", () => {
|
||||
// [
|
||||
// pointFrom(0, 1),
|
||||
// pointFrom(0, -1),
|
||||
// pointFrom(2, 0),
|
||||
// pointFrom(-2, 0),
|
||||
// ].forEach((p) => {
|
||||
// expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||
// });
|
||||
// expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
// expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
it("point on ellipse", () => {
|
||||
[
|
||||
pointFrom(0, 1),
|
||||
pointFrom(0, -1),
|
||||
pointFrom(2, 0),
|
||||
pointFrom(-2, 0),
|
||||
].forEach((p) => {
|
||||
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
|
||||
// expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
// expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
|
||||
|
||||
// expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
// expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
|
||||
// expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
// expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true);
|
||||
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
|
||||
|
||||
// expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false);
|
||||
// expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false);
|
||||
// });
|
||||
expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false);
|
||||
expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false);
|
||||
});
|
||||
|
||||
// it("point in ellipse", () => {
|
||||
// [
|
||||
// pointFrom(0, 1),
|
||||
// pointFrom(0, -1),
|
||||
// pointFrom(2, 0),
|
||||
// pointFrom(-2, 0),
|
||||
// ].forEach((p) => {
|
||||
// expect(pointInEllipse(p, ellipse)).toBe(true);
|
||||
// });
|
||||
it("point in ellipse", () => {
|
||||
[
|
||||
pointFrom(0, 1),
|
||||
pointFrom(0, -1),
|
||||
pointFrom(2, 0),
|
||||
pointFrom(-2, 0),
|
||||
].forEach((p) => {
|
||||
expect(pointInEllipse(p, ellipse)).toBe(true);
|
||||
});
|
||||
|
||||
// expect(pointInEllipse(pointFrom(-1, 0.8), ellipse)).toBe(true);
|
||||
// expect(pointInEllipse(pointFrom(1, -0.8), ellipse)).toBe(true);
|
||||
expect(pointInEllipse(pointFrom(-1, 0.8), ellipse)).toBe(true);
|
||||
expect(pointInEllipse(pointFrom(1, -0.8), ellipse)).toBe(true);
|
||||
|
||||
// expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
|
||||
// expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
|
||||
// });
|
||||
// });
|
||||
expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
|
||||
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("line and line", () => {
|
||||
const lineA: LineSegment<GlobalPoint> = lineSegment(
|
||||
|
Loading…
x
Reference in New Issue
Block a user