Use roughjs to generate the line and freedraw shapes for collision
This commit is contained in:
parent
14a0cd3a97
commit
cd3ca3b4ca
@ -1,8 +1,16 @@
|
||||
import { simplify } from "points-on-curve";
|
||||
|
||||
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
||||
import {
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
type LocalPoint,
|
||||
curve,
|
||||
pointFromArray,
|
||||
} from "@excalidraw/math";
|
||||
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||
@ -31,7 +39,6 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
import type { Drawable, Options } from "roughjs/bin/core";
|
||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
|
||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||
@ -61,6 +68,37 @@ 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,
|
||||
@ -303,6 +341,111 @@ 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) => {
|
||||
const generator = new RoughGenerator();
|
||||
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
|
||||
const points = element.points.length
|
||||
? element.points
|
||||
: [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;
|
||||
}
|
||||
} 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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
return shape.slice(0, element.points.length);
|
||||
}
|
||||
case "freedraw": {
|
||||
const simplifiedPoints = simplify(
|
||||
element.points as Mutable<LocalPoint[]>,
|
||||
0.75,
|
||||
);
|
||||
|
||||
return generator
|
||||
.curve(
|
||||
simplifiedPoints as [number, number][],
|
||||
generateRoughOptionsForCollision(element),
|
||||
)
|
||||
.sets[0].ops.slice(0, element.points.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the roughjs shape for given element.
|
||||
*
|
||||
|
@ -4,7 +4,9 @@ import {
|
||||
arrayToMap,
|
||||
} from "@excalidraw/common";
|
||||
import {
|
||||
curve,
|
||||
curveIntersectLineSegment,
|
||||
isCurve,
|
||||
isPointWithinBounds,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
@ -25,7 +27,7 @@ import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
|
||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { isPathALoop } from "./shapes";
|
||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||
import { getElementBounds } from "./bounds";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isIframeLikeElement,
|
||||
@ -42,6 +44,8 @@ import { getBoundTextElement } from "./textElement";
|
||||
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
||||
import { generateComponentsForCollision } from "./Shape";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawDiamondElement,
|
||||
@ -50,6 +54,8 @@ import type {
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
|
||||
import { debugDrawCubicBezier } from "@excalidraw/excalidraw/visualdebug";
|
||||
|
||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||
if (element.type === "arrow") {
|
||||
return false;
|
||||
@ -101,6 +107,21 @@ 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;
|
||||
@ -195,7 +216,8 @@ export const intersectElementWithLineSegment = (
|
||||
case "line":
|
||||
case "freedraw":
|
||||
case "arrow":
|
||||
throw new Error(`Unimplemented element type '${element.type}'`);
|
||||
return [];
|
||||
//throw new Error(`Unimplemented element type '${element.type}'`);
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user