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 { 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 { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
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";
|
||||||
@ -31,7 +39,6 @@ import type {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
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);
|
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 = (
|
export const generateRoughOptions = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
continuousPath = false,
|
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.
|
* Generates the roughjs shape for given element.
|
||||||
*
|
*
|
||||||
|
@ -4,7 +4,9 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
|
curve,
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
|
isCurve,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
lineSegmentIntersectionPoints,
|
lineSegmentIntersectionPoints,
|
||||||
@ -25,7 +27,7 @@ import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
|
|||||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { isPathALoop } from "./shapes";
|
import { isPathALoop } from "./shapes";
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getElementBounds } from "./bounds";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
@ -42,6 +44,8 @@ import { getBoundTextElement } from "./textElement";
|
|||||||
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
|
||||||
|
import { generateComponentsForCollision } from "./Shape";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
@ -50,6 +54,8 @@ import type {
|
|||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
import { debugDrawCubicBezier } from "@excalidraw/excalidraw/visualdebug";
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
return false;
|
return false;
|
||||||
@ -101,6 +107,21 @@ export const hitElementItself = ({
|
|||||||
: isPointOnShape(point, element, threshold)
|
: isPointOnShape(point, element, threshold)
|
||||||
: false;
|
: 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
|
// hit test against a frame's name
|
||||||
if (!hit && frameNameBound) {
|
if (!hit && frameNameBound) {
|
||||||
const x1 = frameNameBound.x - threshold;
|
const x1 = frameNameBound.x - threshold;
|
||||||
@ -195,7 +216,8 @@ export const intersectElementWithLineSegment = (
|
|||||||
case "line":
|
case "line":
|
||||||
case "freedraw":
|
case "freedraw":
|
||||||
case "arrow":
|
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