State
This commit is contained in:
parent
37ca66044e
commit
6ab3b6c029
@ -1,17 +1,8 @@
|
|||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
|
|
||||||
import {
|
import { pointFrom, type LocalPoint } from "@excalidraw/math";
|
||||||
pointFrom,
|
|
||||||
pointDistance,
|
|
||||||
type LocalPoint,
|
|
||||||
pointRotateRads,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
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 { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||||
@ -25,15 +16,11 @@ import {
|
|||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { getCornerRadius, isPathALoop } from "./shapes";
|
import { getCornerRadius, isPathALoop } from "./shapes";
|
||||||
import { headingForPointIsHorizontal } from "./heading";
|
import { generateElbowArrowRougJshPathCommands } from "./utils";
|
||||||
|
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import { generateFreeDrawShape } from "./renderElement";
|
import { generateFreeDrawShape } from "./renderElement";
|
||||||
import {
|
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
||||||
getArrowheadPoints,
|
|
||||||
getDiamondPoints,
|
|
||||||
getElementBounds,
|
|
||||||
} from "./bounds";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@ -41,11 +28,11 @@ import type {
|
|||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ExcalidrawFreeDrawElement,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
@ -316,125 +303,6 @@ 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.
|
* Generates the roughjs shape for given element.
|
||||||
*
|
*
|
||||||
@ -584,7 +452,7 @@ export const _generateElementShape = (
|
|||||||
} else {
|
} else {
|
||||||
shape = [
|
shape = [
|
||||||
generator.path(
|
generator.path(
|
||||||
generateElbowArrowShape(points, 16),
|
generateElbowArrowRougJshPathCommands(points, 16),
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(element, true),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@ -678,68 +546,3 @@ 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,10 +226,7 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|||||||
: linearElement.endBinding?.elementId;
|
: linearElement.endBinding?.elementId;
|
||||||
if (elementId) {
|
if (elementId) {
|
||||||
const element = elementsMap.get(elementId);
|
const element = elementsMap.get(elementId);
|
||||||
if (
|
if (isBindableElement(element) && bindingBorderTest(element, coors, zoom)) {
|
||||||
isBindableElement(element) &&
|
|
||||||
bindingBorderTest(element, coors, elementsMap, zoom)
|
|
||||||
) {
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -559,7 +556,6 @@ export const getHoveredElementForBinding = (
|
|||||||
bindingBorderTest(
|
bindingBorderTest(
|
||||||
element,
|
element,
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
elementsMap,
|
|
||||||
zoom,
|
zoom,
|
||||||
(fullShape ||
|
(fullShape ||
|
||||||
!isBindingFallthroughEnabled(
|
!isBindingFallthroughEnabled(
|
||||||
@ -592,7 +588,7 @@ export const getHoveredElementForBinding = (
|
|||||||
|
|
||||||
// Prefer the shape with the border being tested (if any)
|
// Prefer the shape with the border being tested (if any)
|
||||||
const borderTestElements = candidateElements.filter((element) =>
|
const borderTestElements = candidateElements.filter((element) =>
|
||||||
bindingBorderTest(element, pointerCoords, elementsMap, zoom, false),
|
bindingBorderTest(element, pointerCoords, zoom, false),
|
||||||
);
|
);
|
||||||
if (borderTestElements.length === 1) {
|
if (borderTestElements.length === 1) {
|
||||||
return borderTestElements[0];
|
return borderTestElements[0];
|
||||||
@ -613,7 +609,6 @@ export const getHoveredElementForBinding = (
|
|||||||
bindingBorderTest(
|
bindingBorderTest(
|
||||||
element,
|
element,
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
elementsMap,
|
|
||||||
zoom,
|
zoom,
|
||||||
// disable fullshape snapping for frame elements so we
|
// disable fullshape snapping for frame elements so we
|
||||||
// can bind to frame children
|
// can bind to frame children
|
||||||
@ -1545,7 +1540,6 @@ const newBoundElements = (
|
|||||||
export const bindingBorderTest = (
|
export const bindingBorderTest = (
|
||||||
element: NonDeleted<ExcalidrawBindableElement>,
|
element: NonDeleted<ExcalidrawBindableElement>,
|
||||||
{ x, y }: { x: number; y: number },
|
{ x, y }: { x: number; y: number },
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
fullShape?: boolean,
|
fullShape?: boolean,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
@ -9,29 +9,22 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import { pointsOnBezierCurves } from "points-on-curve";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Curve,
|
|
||||||
Degrees,
|
Degrees,
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
LocalPoint,
|
||||||
Radians,
|
Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import { getCurvePathOps } from "./utils";
|
||||||
|
|
||||||
import { generateRoughOptions } from "./Shape";
|
import { generateRoughOptions } from "./Shape";
|
||||||
import { ShapeCache } from "./ShapeCache";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
@ -45,13 +38,6 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import { getElementShape } from "./shapes";
|
|
||||||
|
|
||||||
import {
|
|
||||||
deconstructDiamondElement,
|
|
||||||
deconstructRectanguloidElement,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import type {
|
import type {
|
||||||
@ -59,10 +45,8 @@ import type {
|
|||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@ -267,199 +251,6 @@ 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.
|
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||||
*
|
*
|
||||||
@ -868,7 +659,7 @@ const generateLinearElementShape = (
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return generator[method](
|
return generator[method](
|
||||||
element.points as Mutable<LocalPoint>[] as RoughPoint[],
|
element.points as LocalPoint[] as RoughPoint[],
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { simplify } from "points-on-curve";
|
||||||
|
|
||||||
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isTransparent,
|
isTransparent,
|
||||||
elementCenterPoint,
|
elementCenterPoint,
|
||||||
@ -17,43 +21,43 @@ import {
|
|||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
vectorNormalize,
|
vectorNormalize,
|
||||||
vectorScale,
|
vectorScale,
|
||||||
} from "@excalidraw/math";
|
curve,
|
||||||
|
curveCatmullRomCubicApproxPoints,
|
||||||
import {
|
curveOffsetPoints,
|
||||||
|
pointFromArray,
|
||||||
|
rectangle,
|
||||||
ellipse,
|
ellipse,
|
||||||
ellipseSegmentInterceptPoints,
|
ellipseSegmentInterceptPoints,
|
||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Curve,
|
Curve,
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
LineSegment,
|
LineSegment,
|
||||||
|
LocalPoint,
|
||||||
Radians,
|
Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { isPathALoop } from "./shapes";
|
import { getCornerRadius, isPathALoop } from "./shapes";
|
||||||
import { getElementBounds } from "./bounds";
|
import { getDiamondPoints, getElementBounds } from "./bounds";
|
||||||
|
import { getBoundTextElement } from "./textElement";
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { distanceToElement } from "./distance";
|
||||||
|
import { generateElbowArrowRougJshPathCommands } from "./utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
isElbowArrow,
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import {
|
|
||||||
deconstructDiamondElement,
|
|
||||||
deconstructLinearOrFreeDrawElement,
|
|
||||||
deconstructRectanguloidElement,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
import { getBoundTextElement } from "./textElement";
|
import type { Options } from "roughjs/bin/core";
|
||||||
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
|
||||||
|
|
||||||
import { distanceToElement } from "./distance";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
@ -111,9 +115,9 @@ export const hitElementItself = ({
|
|||||||
? shouldTestInside(element)
|
? shouldTestInside(element)
|
||||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||||
// we would need `onShape` as well to include the "borders"
|
// we would need `onShape` as well to include the "borders"
|
||||||
isPointInShape(point, element) ||
|
isPointInElement(point, element) ||
|
||||||
isPointOnShape(point, element, threshold)
|
isPointOnElementOutline(point, element, threshold)
|
||||||
: isPointOnShape(point, element, threshold)
|
: isPointOnElementOutline(point, element, threshold)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
// hit test against a frame's name
|
// hit test against a frame's name
|
||||||
@ -177,7 +181,7 @@ export const hitElementBoundText = (
|
|||||||
}
|
}
|
||||||
: boundTextElementCandidate;
|
: boundTextElementCandidate;
|
||||||
|
|
||||||
return isPointInShape(point, boundTextElement);
|
return isPointInElement(point, boundTextElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -218,19 +222,17 @@ const intersectLinearOrFreeDrawWithLineSegment = (
|
|||||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
segment: LineSegment<GlobalPoint>,
|
segment: LineSegment<GlobalPoint>,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const shapes = deconstructLinearOrFreeDrawElement(element);
|
const shapes = deconstructLinearOrFreeDrawElementForCollision(element);
|
||||||
const intersections: GlobalPoint[] = [];
|
const intersections: GlobalPoint[] = [];
|
||||||
|
|
||||||
for (const shape of shapes) {
|
for (const shape of shapes) {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isCurve(shape):
|
case isCurve(shape):
|
||||||
//debugDrawCubicBezier(shape);
|
|
||||||
intersections.push(
|
intersections.push(
|
||||||
...curveIntersectLineSegment(shape as Curve<GlobalPoint>, segment),
|
...curveIntersectLineSegment(shape as Curve<GlobalPoint>, segment),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
case isLineSegment(shape):
|
case isLineSegment(shape):
|
||||||
//debugDrawLine(shape);
|
|
||||||
const point = lineSegmentIntersectionPoints(
|
const point = lineSegmentIntersectionPoints(
|
||||||
segment,
|
segment,
|
||||||
shape as LineSegment<GlobalPoint>,
|
shape as LineSegment<GlobalPoint>,
|
||||||
@ -267,7 +269,10 @@ const intersectRectanguloidWithLineSegment = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get the element's building components we can test against
|
// Get the element's building components we can test against
|
||||||
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
const [sides, corners] = deconstructRectanguloidElementForCollision(
|
||||||
|
element,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Test intersection against the sides, keep only the valid
|
// Test intersection against the sides, keep only the valid
|
||||||
@ -318,7 +323,10 @@ const intersectDiamondWithLineSegment = (
|
|||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
||||||
const [sides, curves] = deconstructDiamondElement(element, offset);
|
const [sides, curves] = deconstructDiamondElementForCollision(
|
||||||
|
element,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sides
|
sides
|
||||||
@ -371,14 +379,14 @@ const intersectEllipseWithLineSegment = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// check if the given point is considered on the given shape's border
|
// check if the given point is considered on the given shape's border
|
||||||
const isPointOnShape = (
|
const isPointOnElementOutline = (
|
||||||
point: GlobalPoint,
|
point: GlobalPoint,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
tolerance = 1,
|
tolerance = 1,
|
||||||
) => distanceToElement(element, point) <= tolerance;
|
) => distanceToElement(element, point) <= tolerance;
|
||||||
|
|
||||||
// check if the given point is considered inside the element's border
|
// check if the given point is considered inside the element's border
|
||||||
export const isPointInShape = (
|
export const isPointInElement = (
|
||||||
point: GlobalPoint,
|
point: GlobalPoint,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) => {
|
) => {
|
||||||
@ -407,3 +415,437 @@ export const isPointInShape = (
|
|||||||
|
|
||||||
return intersections.length % 2 === 1;
|
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";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deconstructDiamondElement,
|
deconstructDiamondElementForCollision,
|
||||||
deconstructLinearOrFreeDrawElement,
|
deconstructLinearOrFreeDrawElementForCollision,
|
||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElementForCollision,
|
||||||
} from "./utils";
|
} from "./collision";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
@ -75,7 +75,7 @@ const distanceToRectanguloidElement = (
|
|||||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
// Get the element's building components we can test against
|
// Get the element's building components we can test against
|
||||||
const [sides, corners] = deconstructRectanguloidElement(element);
|
const [sides, corners] = deconstructRectanguloidElementForCollision(element);
|
||||||
|
|
||||||
return Math.min(
|
return Math.min(
|
||||||
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
||||||
@ -103,7 +103,7 @@ const distanceToDiamondElement = (
|
|||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
const [sides, curves] = deconstructDiamondElement(element);
|
const [sides, curves] = deconstructDiamondElementForCollision(element);
|
||||||
|
|
||||||
return Math.min(
|
return Math.min(
|
||||||
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
...sides.map((s) => distanceToLineSegment(rotatedPoint, s)),
|
||||||
@ -137,7 +137,7 @@ const distanceToLinearOrFreeDraElement = (
|
|||||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const shapes = deconstructLinearOrFreeDrawElement(element);
|
const shapes = deconstructLinearOrFreeDrawElementForCollision(element);
|
||||||
let distance = Infinity;
|
let distance = Infinity;
|
||||||
|
|
||||||
for (const shape of shapes) {
|
for (const shape of shapes) {
|
||||||
|
@ -15,7 +15,7 @@ import { getElementsWithinSelection, getSelectedElements } from "./selection";
|
|||||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getElementLineSegments,
|
approximateElementWithLineSegments,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
@ -69,9 +69,15 @@ export function isElementIntersectingFrame(
|
|||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) {
|
) {
|
||||||
const frameLineSegments = getElementLineSegments(frame, elementsMap);
|
const frameLineSegments = approximateElementWithLineSegments(
|
||||||
|
frame,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
const elementLineSegments = getElementLineSegments(element, elementsMap);
|
const elementLineSegments = approximateElementWithLineSegments(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||||
elementLineSegments.some((elementLineSegment) =>
|
elementLineSegments.some((elementLineSegment) =>
|
||||||
|
@ -9,8 +9,6 @@ import {
|
|||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DRAGGING_THRESHOLD,
|
DRAGGING_THRESHOLD,
|
||||||
KEYS,
|
KEYS,
|
||||||
@ -20,7 +18,7 @@ import {
|
|||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Store } from "@excalidraw/element";
|
import { getCurvePathOps, type Store } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
|
@ -16,116 +16,21 @@ import {
|
|||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "@excalidraw/math";
|
} 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 type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { shouldTestInside } from "./collision";
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
|
||||||
import { getBoundTextElement } from "./textElement";
|
|
||||||
import { ShapeCache } from "./ShapeCache";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, type Bounds } from "./bounds";
|
import { getCurvePathOps } from "./utils";
|
||||||
|
|
||||||
|
import type { Bounds } from "./bounds";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./types";
|
} 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 = <
|
export const getControlPointsForBezierCurve = <
|
||||||
P extends GlobalPoint | LocalPoint,
|
P extends GlobalPoint | LocalPoint,
|
||||||
>(
|
>(
|
||||||
|
@ -1,341 +1,81 @@
|
|||||||
import {
|
import { pointDistance } from "@excalidraw/math";
|
||||||
curve,
|
|
||||||
curveCatmullRomCubicApproxPoints,
|
|
||||||
curveOffsetPoints,
|
|
||||||
lineSegment,
|
|
||||||
pointFrom,
|
|
||||||
pointFromArray,
|
|
||||||
rectangle,
|
|
||||||
type GlobalPoint,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCornerRadius } from "./shapes";
|
import { headingForPointIsHorizontal } from "./heading";
|
||||||
|
|
||||||
import { getDiamondPoints } from "./bounds";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
|
|
||||||
import { generateLinearCollisionShape } from "./Shape";
|
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 type {
|
export const generateElbowArrowRougJshPathCommands = (
|
||||||
ExcalidrawDiamondElement,
|
points: readonly LocalPoint[],
|
||||||
ExcalidrawFreeDrawElement,
|
radius: number,
|
||||||
ExcalidrawLinearElement,
|
) => {
|
||||||
ExcalidrawRectanguloidElement,
|
const subpoints = [] as [number, number][];
|
||||||
} from "./types";
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
export function deconstructLinearOrFreeDrawElement(
|
if (prevIsHorizontal) {
|
||||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
if (prev[0] < point[0]) {
|
||||||
): (Curve<GlobalPoint> | LineSegment<GlobalPoint>)[] {
|
// LEFT
|
||||||
const ops = generateLinearCollisionShape(element) as {
|
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||||
op: string;
|
} else {
|
||||||
data: number[];
|
// RIGHT
|
||||||
}[];
|
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||||
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return components;
|
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(
|
||||||
* Get the building components of a rectanguloid element in the form of
|
`Q ${subpoints[i + 1][0]} ${subpoints[i + 1][1]}, ${
|
||||||
* line segments and curves.
|
subpoints[i + 2][0]
|
||||||
*
|
} ${subpoints[i + 2][1]}`,
|
||||||
* @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;
|
|
||||||
}
|
}
|
||||||
|
d.push(`L ${points[points.length - 1][0]} ${points[points.length - 1][1]}`);
|
||||||
|
|
||||||
const r = rectangle(
|
return d.join(" ");
|
||||||
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,
|
refreshTextDimensions,
|
||||||
deepCopyElement,
|
deepCopyElement,
|
||||||
duplicateElements,
|
duplicateElements,
|
||||||
isPointInShape,
|
isPointInElement,
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@ -5164,7 +5164,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
// if hitting the bounding box, return early
|
// if hitting the bounding box, return early
|
||||||
// but if not, we should check for other cases as well (e.g. frame name)
|
// but if not, we should check for other cases as well (e.g. frame name)
|
||||||
if (isPointInShape(pointFrom(x, y), element)) {
|
if (isPointInElement(pointFrom(x, y), element)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||||
import { getElementLineSegments, isPointInShape } from "@excalidraw/element";
|
|
||||||
import {
|
import {
|
||||||
lineSegment,
|
getBoundTextElement,
|
||||||
lineSegmentIntersectionPoints,
|
intersectElementWithLineSegment,
|
||||||
pointFrom,
|
isPointInElement,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/element";
|
||||||
|
import { lineSegment, pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getElementsInGroup } from "@excalidraw/element";
|
import { getElementsInGroup } from "@excalidraw/element";
|
||||||
|
|
||||||
@ -12,12 +12,7 @@ import { shouldTestInside } from "@excalidraw/element";
|
|||||||
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
||||||
import { getBoundTextElementId } from "@excalidraw/element";
|
import { getBoundTextElementId } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||||
import type {
|
|
||||||
ElementsSegmentsMap,
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
} from "@excalidraw/math/types";
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { AnimatedTrail } from "../animated-trail";
|
import { AnimatedTrail } from "../animated-trail";
|
||||||
@ -33,8 +28,6 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
||||||
private geometricShapesCache: Map<string, GeometricShape<GlobalPoint>> =
|
|
||||||
new Map();
|
|
||||||
|
|
||||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||||
super(animationFrameHandler, app, {
|
super(animationFrameHandler, app, {
|
||||||
@ -110,8 +103,6 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
const intersects = eraserTest(
|
const intersects = eraserTest(
|
||||||
pathSegments,
|
pathSegments,
|
||||||
element,
|
element,
|
||||||
this.segmentsCache,
|
|
||||||
this.geometricShapesCache,
|
|
||||||
candidateElementsMap,
|
candidateElementsMap,
|
||||||
this.app,
|
this.app,
|
||||||
);
|
);
|
||||||
@ -148,8 +139,6 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
const intersects = eraserTest(
|
const intersects = eraserTest(
|
||||||
pathSegments,
|
pathSegments,
|
||||||
element,
|
element,
|
||||||
this.segmentsCache,
|
|
||||||
this.geometricShapesCache,
|
|
||||||
candidateElementsMap,
|
candidateElementsMap,
|
||||||
this.app,
|
this.app,
|
||||||
);
|
);
|
||||||
@ -201,31 +190,23 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
const eraserTest = (
|
const eraserTest = (
|
||||||
pathSegments: LineSegment<GlobalPoint>[],
|
pathSegments: LineSegment<GlobalPoint>[],
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsSegments: ElementsSegmentsMap,
|
|
||||||
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
app: App,
|
app: App,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||||
if (shouldTestInside(element) && isPointInShape(lastPoint, element)) {
|
if (shouldTestInside(element) && isPointInElement(lastPoint, element)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let elementSegments = elementsSegments.get(element.id);
|
const offset = app.getElementHitThreshold();
|
||||||
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
if (!elementSegments) {
|
return pathSegments.some(
|
||||||
elementSegments = getElementLineSegments(element, elementsMap);
|
(pathSegment) =>
|
||||||
elementsSegments.set(element.id, elementSegments);
|
intersectElementWithLineSegment(element, pathSegment, offset).length >
|
||||||
}
|
0 ||
|
||||||
|
(boundTextElement &&
|
||||||
return pathSegments.some((pathSegment) =>
|
intersectElementWithLineSegment(boundTextElement, pathSegment, offset)
|
||||||
elementSegments?.some(
|
.length > 0),
|
||||||
(elementSegment) =>
|
|
||||||
lineSegmentIntersectionPoints(
|
|
||||||
pathSegment,
|
|
||||||
elementSegment,
|
|
||||||
app.getElementHitThreshold(),
|
|
||||||
) !== null,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
pointFrom,
|
pointFrom,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getElementLineSegments } from "@excalidraw/element";
|
import { approximateElementWithLineSegments } from "@excalidraw/element";
|
||||||
import { LinearElementEditor } from "@excalidraw/element";
|
import { LinearElementEditor } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
@ -190,7 +190,10 @@ export class LassoTrail extends AnimatedTrail {
|
|||||||
this.elementsSegments = new Map();
|
this.elementsSegments = new Map();
|
||||||
const visibleElementsMap = arrayToMap(this.app.visibleElements);
|
const visibleElementsMap = arrayToMap(this.app.visibleElements);
|
||||||
for (const element of this.app.visibleElements) {
|
for (const element of this.app.visibleElements) {
|
||||||
const segments = getElementLineSegments(element, visibleElementsMap);
|
const segments = approximateElementWithLineSegments(
|
||||||
|
element,
|
||||||
|
visibleElementsMap,
|
||||||
|
);
|
||||||
this.elementsSegments.set(element.id, segments);
|
this.elementsSegments.set(element.id, segments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,12 @@ import type {
|
|||||||
LineSegment,
|
LineSegment,
|
||||||
} from "@excalidraw/math/types";
|
} from "@excalidraw/math/types";
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
import { intersectElementWithLineSegment } from "@excalidraw/element";
|
||||||
|
import App from "../components/App";
|
||||||
|
|
||||||
export const getLassoSelectedElementIds = (input: {
|
export const getLassoSelectedElementIds = (input: {
|
||||||
lassoPath: GlobalPoint[];
|
lassoPath: GlobalPoint[];
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
elementsSegments: ElementsSegmentsMap;
|
|
||||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||||
simplifyDistance?: number;
|
simplifyDistance?: number;
|
||||||
@ -27,7 +28,6 @@ export const getLassoSelectedElementIds = (input: {
|
|||||||
const {
|
const {
|
||||||
lassoPath,
|
lassoPath,
|
||||||
elements,
|
elements,
|
||||||
elementsSegments,
|
|
||||||
intersectedElements,
|
intersectedElements,
|
||||||
enclosedElements,
|
enclosedElements,
|
||||||
simplifyDistance,
|
simplifyDistance,
|
||||||
@ -48,7 +48,7 @@ export const getLassoSelectedElementIds = (input: {
|
|||||||
if (enclosed) {
|
if (enclosed) {
|
||||||
enclosedElements.add(element.id);
|
enclosedElements.add(element.id);
|
||||||
} else {
|
} else {
|
||||||
const intersects = intersectionTest(path, element, elementsSegments);
|
const intersects = intersectionTest(path, element, app);
|
||||||
if (intersects) {
|
if (intersects) {
|
||||||
intersectedElements.add(element.id);
|
intersectedElements.add(element.id);
|
||||||
}
|
}
|
||||||
@ -66,13 +66,13 @@ export const getLassoSelectedElementIds = (input: {
|
|||||||
const enclosureTest = (
|
const enclosureTest = (
|
||||||
lassoPath: GlobalPoint[],
|
lassoPath: GlobalPoint[],
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsSegments: ElementsSegmentsMap,
|
app: App,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const lassoPolygon = polygonFromPoints(lassoPath);
|
const lassoSegments = lassoPath
|
||||||
const segments = elementsSegments.get(element.id);
|
.slice(1)
|
||||||
if (!segments) {
|
.map((point, index) => lineSegment(lassoPath[index], point))
|
||||||
return false;
|
.concat(lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0]));
|
||||||
}
|
const offset = app.getElementHitThreshold();
|
||||||
|
|
||||||
return segments.some((segment) => {
|
return segments.some((segment) => {
|
||||||
return segment.some((point) =>
|
return segment.some((point) =>
|
||||||
@ -84,26 +84,15 @@ const enclosureTest = (
|
|||||||
const intersectionTest = (
|
const intersectionTest = (
|
||||||
lassoPath: GlobalPoint[],
|
lassoPath: GlobalPoint[],
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsSegments: ElementsSegmentsMap,
|
app: App,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const elementSegments = elementsSegments.get(element.id);
|
const lassoSegments = lassoPath
|
||||||
if (!elementSegments) {
|
.slice(1)
|
||||||
return false;
|
.map((point, index) => lineSegment(lassoPath[index], point))
|
||||||
}
|
.concat(lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0]));
|
||||||
|
const offset = app.getElementHitThreshold();
|
||||||
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) =>
|
return lassoSegments.some((lassoSegment) =>
|
||||||
elementSegments.some(
|
intersectElementWithLineSegment(element, lassoSegment, offset),
|
||||||
(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,
|
type ElementsSegmentsMap,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getElementLineSegments } from "@excalidraw/element";
|
import { approximateElementWithLineSegments } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => {
|
|||||||
|
|
||||||
const elementsSegments: ElementsSegmentsMap = new Map();
|
const elementsSegments: ElementsSegmentsMap = new Map();
|
||||||
for (const element of h.elements) {
|
for (const element of h.elements) {
|
||||||
const segments = getElementLineSegments(
|
const segments = approximateElementWithLineSegments(
|
||||||
element,
|
element,
|
||||||
h.app.scene.getElementsMapIncludingDeleted(),
|
h.app.scene.getElementsMapIncludingDeleted(),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./angle";
|
export * from "./angle";
|
||||||
export * from "./curve";
|
export * from "./curve";
|
||||||
|
export * from "./ellipse";
|
||||||
export * from "./line";
|
export * from "./line";
|
||||||
export * from "./point";
|
export * from "./point";
|
||||||
export * from "./polygon";
|
export * from "./polygon";
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,14 +8,7 @@ import {
|
|||||||
segmentsIntersectAt,
|
segmentsIntersectAt,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type { GlobalPoint, LineSegment, Polygon } from "@excalidraw/math";
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
Polygon,
|
|
||||||
Radians,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
|
|
||||||
import { pointInEllipse, pointOnEllipse, type Ellipse } from "../src/shape";
|
|
||||||
|
|
||||||
describe("point and line", () => {
|
describe("point and line", () => {
|
||||||
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
|
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
|
||||||
@ -71,56 +64,56 @@ describe("point and polygon", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("point and ellipse", () => {
|
// describe("point and ellipse", () => {
|
||||||
const ellipse: Ellipse<GlobalPoint> = {
|
// const ellipse: Ellipse<GlobalPoint> = {
|
||||||
center: pointFrom(0, 0),
|
// center: pointFrom(0, 0),
|
||||||
angle: 0 as Radians,
|
// angle: 0 as Radians,
|
||||||
halfWidth: 2,
|
// halfWidth: 2,
|
||||||
halfHeight: 1,
|
// halfHeight: 1,
|
||||||
};
|
// };
|
||||||
|
|
||||||
it("point on ellipse", () => {
|
// it("point on ellipse", () => {
|
||||||
[
|
// [
|
||||||
pointFrom(0, 1),
|
// pointFrom(0, 1),
|
||||||
pointFrom(0, -1),
|
// pointFrom(0, -1),
|
||||||
pointFrom(2, 0),
|
// pointFrom(2, 0),
|
||||||
pointFrom(-2, 0),
|
// pointFrom(-2, 0),
|
||||||
].forEach((p) => {
|
// ].forEach((p) => {
|
||||||
expect(pointOnEllipse(p, ellipse)).toBe(true);
|
// expect(pointOnEllipse(p, ellipse)).toBe(true);
|
||||||
});
|
// });
|
||||||
expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).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.71), ellipse, 0.01)).toBe(true);
|
||||||
|
|
||||||
expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).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.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.1)).toBe(true);
|
||||||
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).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.1)).toBe(true);
|
||||||
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).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", () => {
|
// it("point in ellipse", () => {
|
||||||
[
|
// [
|
||||||
pointFrom(0, 1),
|
// pointFrom(0, 1),
|
||||||
pointFrom(0, -1),
|
// pointFrom(0, -1),
|
||||||
pointFrom(2, 0),
|
// pointFrom(2, 0),
|
||||||
pointFrom(-2, 0),
|
// pointFrom(-2, 0),
|
||||||
].forEach((p) => {
|
// ].forEach((p) => {
|
||||||
expect(pointInEllipse(p, ellipse)).toBe(true);
|
// 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, 1), ellipse)).toBe(false);
|
||||||
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
|
// expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
describe("line and line", () => {
|
describe("line and line", () => {
|
||||||
const lineA: LineSegment<GlobalPoint> = lineSegment(
|
const lineA: LineSegment<GlobalPoint> = lineSegment(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user