Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs 2025-05-07 22:18:03 +02:00
parent cd3ca3b4ca
commit 3b1c6444e2
No known key found for this signature in database
3 changed files with 146 additions and 138 deletions

View File

@ -1,12 +1,6 @@
import { simplify } from "points-on-curve";
import {
pointFrom,
pointDistance,
type LocalPoint,
curve,
pointFromArray,
} from "@excalidraw/math";
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
import { RoughGenerator } from "roughjs/bin/generator";
@ -36,6 +30,7 @@ import type {
ExcalidrawSelectionElement,
ExcalidrawLinearElement,
Arrowhead,
ExcalidrawFreeDrawElement,
} from "./types";
import type { Drawable, Options } from "roughjs/bin/core";
@ -68,37 +63,6 @@ function adjustRoughness(element: ExcalidrawElement): number {
return Math.min(roughness / (maxSize < 10 ? 3 : 2), 2.5);
}
export const generateRoughOptionsForCollision = (
element: ExcalidrawElement,
): Options => {
const options: Options = {
seed: element.seed,
disableMultiStroke: true,
roughness: 0,
preserveVertices: true,
};
switch (element.type) {
case "rectangle":
case "iframe":
case "embeddable":
case "diamond":
case "ellipse": {
if (element.type === "ellipse") {
options.curveFitting = 1;
}
return options;
}
case "line":
case "freedraw":
case "arrow":
return options;
default: {
throw new Error(`Unimplemented type ${element.type}`);
}
}
};
export const generateRoughOptions = (
element: ExcalidrawElement,
continuousPath = false,
@ -341,50 +305,22 @@ const getArrowheadShapes = (
}
};
export const generateComponentsForCollision = (element: ExcalidrawElement) => {
const ops = generateRoughOpsForCollision(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 "bcurveTo":
if (!prevPoint) {
throw new Error("prevPoint is undefined");
}
components.push(
curve(
prevPoint,
pointFrom<LocalPoint>(op.data[0], op.data[1]),
pointFrom<LocalPoint>(op.data[2], op.data[3]),
pointFrom<LocalPoint>(op.data[4], op.data[5]),
),
);
continue;
default: {
console.error("Unknown op type", op.op);
}
}
}
return components;
};
const generateRoughOpsForCollision = (element: ExcalidrawElement) => {
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": {
let shape: any;
const options = generateRoughOptions(element);
// points array can be empty in the beginning, so it is important to add
// initial position to it
@ -393,42 +329,24 @@ const generateRoughOpsForCollision = (element: ExcalidrawElement) => {
: [pointFrom<LocalPoint>(0, 0)];
if (isElbowArrow(element)) {
// NOTE (mtolmacs): Temporary fix for extremely big arrow shapes
if (
!points.every(
(point) => Math.abs(point[0]) <= 1e6 && Math.abs(point[1]) <= 1e6,
)
) {
console.error(
`Elbow arrow with extreme point positions detected. Arrow not rendered.`,
element.id,
JSON.stringify(points),
);
shape = [];
} else {
shape = generator.path(
generateElbowArrowShape(points, 16),
generateRoughOptionsForCollision(element),
).sets[0].ops;
}
shape = generator.path(generateElbowArrowShape(points, 16), options)
.sets[0].ops;
} else if (!element.roundness) {
// curve is always the first element
// this simplifies finding the curve for an element
if (options.fill) {
shape = generator.polygon(points as unknown as RoughPoint[], options)
.sets[0].ops;
shape = points.map((point, idx) => {
return idx === 0
? { op: "move", data: point }
: {
op: "lineTo",
data: [point[0], point[1]],
};
});
} else {
shape = generator.linearPath(
points as unknown as RoughPoint[],
options,
).sets[0].ops;
}
} else {
shape = generator.curve(points as unknown as RoughPoint[], options)
.sets[0].ops;
shape = generator
.curve(points as unknown as RoughPoint[], options)
.sets[0].ops.slice(0, element.points.length);
}
return shape.slice(0, element.points.length);
return shape;
}
case "freedraw": {
const simplifiedPoints = simplify(
@ -437,10 +355,7 @@ const generateRoughOpsForCollision = (element: ExcalidrawElement) => {
);
return generator
.curve(
simplifiedPoints as [number, number][],
generateRoughOptionsForCollision(element),
)
.curve(simplifiedPoints as [number, number][], options)
.sets[0].ops.slice(0, element.points.length);
}
}

View File

@ -4,9 +4,9 @@ import {
arrayToMap,
} from "@excalidraw/common";
import {
curve,
curveIntersectLineSegment,
isCurve,
isLineSegment,
isPointWithinBounds,
lineSegment,
lineSegmentIntersectionPoints,
@ -22,7 +22,12 @@ import {
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
import type {
Curve,
GlobalPoint,
LineSegment,
Radians,
} from "@excalidraw/math";
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
@ -37,6 +42,7 @@ import {
} from "./typeChecks";
import {
deconstructDiamondElement,
deconstructLinearOrFreeDrawElement,
deconstructRectanguloidElement,
} from "./utils";
@ -44,18 +50,16 @@ import { getBoundTextElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { generateComponentsForCollision } from "./Shape";
import type {
ElementsMap,
ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawEllipseElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
ExcalidrawRectanguloidElement,
} from "./types";
import { debugDrawCubicBezier } from "@excalidraw/excalidraw/visualdebug";
export const shouldTestInside = (element: ExcalidrawElement) => {
if (element.type === "arrow") {
return false;
@ -107,21 +111,6 @@ export const hitElementItself = ({
: isPointOnShape(point, element, threshold)
: false;
element.type === "freedraw" &&
generateComponentsForCollision(element).forEach((c) => {
if (isCurve(c)) {
debugDrawCubicBezier(
curve(
pointFrom<GlobalPoint>(element.x + c[0][0], element.y + c[0][1]),
pointFrom<GlobalPoint>(element.x + c[1][0], element.y + c[1][1]),
pointFrom<GlobalPoint>(element.x + c[2][0], element.y + c[2][1]),
pointFrom<GlobalPoint>(element.x + c[3][0], element.y + c[3][1]),
),
{ color: "red" },
);
}
});
// hit test against a frame's name
if (!hit && frameNameBound) {
const x1 = frameNameBound.x - threshold;
@ -216,11 +205,41 @@ export const intersectElementWithLineSegment = (
case "line":
case "freedraw":
case "arrow":
return [];
//throw new Error(`Unimplemented element type '${element.type}'`);
return intersectLinearOrFreeDrawWithLineSegment(element, line);
}
};
const intersectLinearOrFreeDrawWithLineSegment = (
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
segment: LineSegment<GlobalPoint>,
): GlobalPoint[] => {
const shapes = deconstructLinearOrFreeDrawElement(element);
const intersections: GlobalPoint[] = [];
for (const shape of shapes) {
switch (true) {
case isCurve(shape):
intersections.push(
...curveIntersectLineSegment(shape as Curve<GlobalPoint>, segment),
);
continue;
case isLineSegment(shape):
const point = lineSegmentIntersectionPoints(
segment,
shape as LineSegment<GlobalPoint>,
);
if (point) {
intersections.push(point);
}
continue;
}
}
return intersections;
};
const intersectRectanguloidWithLineSegment = (
element: ExcalidrawRectanguloidElement,
l: LineSegment<GlobalPoint>,

View File

@ -2,6 +2,7 @@ import {
curve,
lineSegment,
pointFrom,
pointFromArray,
pointFromVector,
rectangle,
vectorFromPoint,
@ -12,17 +13,90 @@ import {
import { elementCenterPoint } from "@excalidraw/common";
import type { Curve, LineSegment } from "@excalidraw/math";
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
import { getCornerRadius } from "./shapes";
import { getDiamondPoints } from "./bounds";
import { generateLinearCollisionShape } from "./Shape";
import type {
ExcalidrawDiamondElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
ExcalidrawRectanguloidElement,
} from "./types";
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);
}
}
}
return components;
}
/**
* Get the building components of a rectanguloid element in the form of
* line segments and curves.