mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Removing workers as the complexit is much worse, while perf. does not seem to be much better
This commit is contained in:
parent
aab03481a2
commit
57b4a098f3
6 changed files with 116 additions and 493 deletions
|
@ -29,10 +29,7 @@ import { type AnimationFrameHandler } from "../animation-frame-handler";
|
||||||
|
|
||||||
import { AnimatedTrail } from "../animated-trail";
|
import { AnimatedTrail } from "../animated-trail";
|
||||||
|
|
||||||
import {
|
import { getLassoSelectedElementIds } from "./utils";
|
||||||
getLassoSelectedElementIds,
|
|
||||||
type LassoWorkerInput,
|
|
||||||
} from "./lasso-main";
|
|
||||||
|
|
||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
|
|
||||||
|
@ -183,17 +180,14 @@ export class LassoTrail extends AnimatedTrail {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lassoPath) {
|
if (lassoPath) {
|
||||||
// need to omit command, otherwise "shared" chunk will be included in the main bundle by default
|
const { selectedElementIds } = getLassoSelectedElementIds({
|
||||||
const message: Omit<LassoWorkerInput, "command"> = {
|
|
||||||
lassoPath,
|
lassoPath,
|
||||||
elements: this.app.visibleElements,
|
elements: this.app.visibleElements,
|
||||||
elementsSegments: this.elementsSegments,
|
elementsSegments: this.elementsSegments,
|
||||||
intersectedElements: this.intersectedElements,
|
intersectedElements: this.intersectedElements,
|
||||||
enclosedElements: this.enclosedElements,
|
enclosedElements: this.enclosedElements,
|
||||||
simplifyDistance: 5 / this.app.state.zoom.value,
|
simplifyDistance: 5 / this.app.state.zoom.value,
|
||||||
};
|
});
|
||||||
|
|
||||||
const { selectedElementIds } = await getLassoSelectedElementIds(message);
|
|
||||||
|
|
||||||
this.selectElementsFromIds(selectedElementIds);
|
this.selectElementsFromIds(selectedElementIds);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
import { promiseTry } from "@excalidraw/common";
|
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import type { GlobalPoint } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import { WorkerPool } from "../workers";
|
|
||||||
|
|
||||||
import type { Commands, ElementsSegmentsMap } from "./lasso-shared.chunk";
|
|
||||||
|
|
||||||
let shouldUseWorkers = typeof Worker !== "undefined";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to get the selected element with a worker, if it fails, it fallbacks to the main thread.
|
|
||||||
*
|
|
||||||
* @param input - The input data for the lasso selection.
|
|
||||||
* @returns The selected element ids.
|
|
||||||
*/
|
|
||||||
export const getLassoSelectedElementIds = async (
|
|
||||||
input: Omit<LassoWorkerInput, "command">,
|
|
||||||
): Promise<
|
|
||||||
LassoWorkerOutput<typeof Commands.GET_LASSO_SELECTED_ELEMENT_IDS>
|
|
||||||
> => {
|
|
||||||
const { Commands, getLassoSelectedElementIds } =
|
|
||||||
await lazyLoadLassoSharedChunk();
|
|
||||||
|
|
||||||
const inputWithCommand: LassoWorkerInput = {
|
|
||||||
...input,
|
|
||||||
command: Commands.GET_LASSO_SELECTED_ELEMENT_IDS,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!shouldUseWorkers) {
|
|
||||||
return getLassoSelectedElementIds(inputWithCommand);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promiseTry(async () => {
|
|
||||||
try {
|
|
||||||
const workerPool = await getOrCreateWorkerPool();
|
|
||||||
|
|
||||||
const result = await workerPool.postMessage(inputWithCommand, {});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
// don't use workers if they are failing
|
|
||||||
shouldUseWorkers = false;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(
|
|
||||||
"Failed to use workers for lasso selection, falling back to the main thread.",
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
|
|
||||||
// fallback to the main thread
|
|
||||||
return getLassoSelectedElementIds(inputWithCommand);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// lazy-loaded and cached chunks
|
|
||||||
let lassoWorker: Promise<typeof import("./lasso-worker.chunk")> | null = null;
|
|
||||||
let lassoShared: Promise<typeof import("./lasso-shared.chunk")> | null = null;
|
|
||||||
|
|
||||||
export const lazyLoadLassoWorkerChunk = async () => {
|
|
||||||
if (!lassoWorker) {
|
|
||||||
lassoWorker = import("./lasso-worker.chunk");
|
|
||||||
}
|
|
||||||
|
|
||||||
return lassoWorker;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const lazyLoadLassoSharedChunk = async () => {
|
|
||||||
if (!lassoShared) {
|
|
||||||
lassoShared = import("./lasso-shared.chunk");
|
|
||||||
}
|
|
||||||
|
|
||||||
return lassoShared;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LassoWorkerInput = {
|
|
||||||
command: typeof Commands.GET_LASSO_SELECTED_ELEMENT_IDS;
|
|
||||||
lassoPath: GlobalPoint[];
|
|
||||||
elements: readonly ExcalidrawElement[];
|
|
||||||
elementsSegments: ElementsSegmentsMap;
|
|
||||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
|
||||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
|
||||||
simplifyDistance?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LassoWorkerOutput<T extends LassoWorkerInput["command"]> =
|
|
||||||
T extends typeof Commands.GET_LASSO_SELECTED_ELEMENT_IDS
|
|
||||||
? {
|
|
||||||
selectedElementIds: string[];
|
|
||||||
}
|
|
||||||
: never;
|
|
||||||
|
|
||||||
let workerPool: Promise<
|
|
||||||
WorkerPool<LassoWorkerInput, LassoWorkerOutput<LassoWorkerInput["command"]>>
|
|
||||||
> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazy initialize or get the worker pool singleton.
|
|
||||||
*
|
|
||||||
* @throws implicitly if anything goes wrong
|
|
||||||
*/
|
|
||||||
const getOrCreateWorkerPool = () => {
|
|
||||||
if (!workerPool) {
|
|
||||||
// immediate concurrent-friendly return, to ensure we have only one pool instance
|
|
||||||
workerPool = promiseTry(async () => {
|
|
||||||
const { WorkerUrl } = await lazyLoadLassoWorkerChunk();
|
|
||||||
|
|
||||||
const pool = WorkerPool.create<
|
|
||||||
LassoWorkerInput,
|
|
||||||
LassoWorkerOutput<LassoWorkerInput["command"]>
|
|
||||||
>(WorkerUrl, {
|
|
||||||
// limit the pool size to a single active worker
|
|
||||||
maxPoolSize: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return workerPool;
|
|
||||||
};
|
|
|
@ -1,329 +0,0 @@
|
||||||
import type {
|
|
||||||
GlobalPoint,
|
|
||||||
Line,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Polygon,
|
|
||||||
} from "@excalidraw/math/types";
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import type { LassoWorkerInput, LassoWorkerOutput } from "./lasso-main";
|
|
||||||
|
|
||||||
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared commands between the main thread and worker threads.
|
|
||||||
*/
|
|
||||||
export const Commands = {
|
|
||||||
GET_LASSO_SELECTED_ELEMENT_IDS: "GET_LASSO_SELECTED_ELEMENT_IDS",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const getLassoSelectedElementIds = (
|
|
||||||
input: LassoWorkerInput,
|
|
||||||
): LassoWorkerOutput<typeof Commands.GET_LASSO_SELECTED_ELEMENT_IDS> => {
|
|
||||||
const {
|
|
||||||
lassoPath,
|
|
||||||
elements,
|
|
||||||
elementsSegments,
|
|
||||||
intersectedElements,
|
|
||||||
enclosedElements,
|
|
||||||
simplifyDistance,
|
|
||||||
} = input;
|
|
||||||
// simplify the path to reduce the number of points
|
|
||||||
let path: GlobalPoint[] = lassoPath;
|
|
||||||
if (simplifyDistance) {
|
|
||||||
path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
|
|
||||||
}
|
|
||||||
// close the path to form a polygon for enclosure check
|
|
||||||
const closedPath = polygonFromPoints(path);
|
|
||||||
// as the path might not enclose a shape anymore, clear before checking
|
|
||||||
enclosedElements.clear();
|
|
||||||
for (const element of elements) {
|
|
||||||
if (
|
|
||||||
!intersectedElements.has(element.id) &&
|
|
||||||
!enclosedElements.has(element.id)
|
|
||||||
) {
|
|
||||||
const enclosed = enclosureTest(closedPath, element, elementsSegments);
|
|
||||||
if (enclosed) {
|
|
||||||
enclosedElements.add(element.id);
|
|
||||||
} else {
|
|
||||||
const intersects = intersectionTest(
|
|
||||||
closedPath,
|
|
||||||
element,
|
|
||||||
elementsSegments,
|
|
||||||
);
|
|
||||||
if (intersects) {
|
|
||||||
intersectedElements.add(element.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = [...intersectedElements, ...enclosedElements];
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedElementIds: results,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const enclosureTest = (
|
|
||||||
lassoPath: GlobalPoint[],
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
elementsSegments: ElementsSegmentsMap,
|
|
||||||
): boolean => {
|
|
||||||
const lassoPolygon = polygonFromPoints(lassoPath);
|
|
||||||
const segments = elementsSegments.get(element.id);
|
|
||||||
if (!segments) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments.some((segment) => {
|
|
||||||
return segment.some((point) => polygonIncludesPoint(point, lassoPolygon));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const intersectionTest = (
|
|
||||||
lassoPath: GlobalPoint[],
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
elementsSegments: ElementsSegmentsMap,
|
|
||||||
): boolean => {
|
|
||||||
const elementSegments = elementsSegments.get(element.id);
|
|
||||||
if (!elementSegments) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
acc.push(lineSegment(lassoPath[index - 1], point));
|
|
||||||
return acc;
|
|
||||||
}, [] as LineSegment<GlobalPoint>[]);
|
|
||||||
|
|
||||||
return lassoSegments.some((lassoSegment) =>
|
|
||||||
elementSegments.some(
|
|
||||||
(elementSegment) =>
|
|
||||||
// introduce a bit of tolerance to account for roughness and simplification of paths
|
|
||||||
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function polygonFromPoints<Point extends GlobalPoint | LocalPoint>(
|
|
||||||
points: Point[],
|
|
||||||
) {
|
|
||||||
return polygonClose(points) as Polygon<Point>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function polygonClose<Point extends LocalPoint | GlobalPoint>(
|
|
||||||
polygon: Point[],
|
|
||||||
) {
|
|
||||||
return polygonIsClosed(polygon)
|
|
||||||
? polygon
|
|
||||||
: ([...polygon, polygon[0]] as Polygon<Point>);
|
|
||||||
}
|
|
||||||
|
|
||||||
function polygonIsClosed<Point extends LocalPoint | GlobalPoint>(
|
|
||||||
polygon: Point[],
|
|
||||||
) {
|
|
||||||
return pointsEqual(polygon[0], polygon[polygon.length - 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRECISION = 10e-5;
|
|
||||||
|
|
||||||
function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
|
||||||
a: Point,
|
|
||||||
b: Point,
|
|
||||||
): boolean {
|
|
||||||
const abs = Math.abs;
|
|
||||||
return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
|
|
||||||
}
|
|
||||||
|
|
||||||
const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
|
|
||||||
point: Point,
|
|
||||||
polygon: Polygon<Point>,
|
|
||||||
) => {
|
|
||||||
const x = point[0];
|
|
||||||
const y = point[1];
|
|
||||||
let inside = false;
|
|
||||||
|
|
||||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
||||||
const xi = polygon[i][0];
|
|
||||||
const yi = polygon[i][1];
|
|
||||||
const xj = polygon[j][0];
|
|
||||||
const yj = polygon[j][1];
|
|
||||||
|
|
||||||
if (
|
|
||||||
((yi > y && yj <= y) || (yi <= y && yj > y)) &&
|
|
||||||
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
|
|
||||||
) {
|
|
||||||
inside = !inside;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inside;
|
|
||||||
};
|
|
||||||
|
|
||||||
function lineSegment<P extends GlobalPoint | LocalPoint>(
|
|
||||||
a: P,
|
|
||||||
b: P,
|
|
||||||
): LineSegment<P> {
|
|
||||||
return [a, b] as LineSegment<P>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lineSegmentIntersectionPoints<Point extends GlobalPoint | LocalPoint>(
|
|
||||||
l: LineSegment<Point>,
|
|
||||||
s: LineSegment<Point>,
|
|
||||||
threshold?: number,
|
|
||||||
): Point | null {
|
|
||||||
const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
|
|
||||||
|
|
||||||
if (
|
|
||||||
!candidate ||
|
|
||||||
!pointOnLineSegment(candidate, s, threshold) ||
|
|
||||||
!pointOnLineSegment(candidate, l, threshold)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
function linesIntersectAt<Point extends GlobalPoint | LocalPoint>(
|
|
||||||
a: Line<Point>,
|
|
||||||
b: Line<Point>,
|
|
||||||
): Point | null {
|
|
||||||
const A1 = a[1][1] - a[0][1];
|
|
||||||
const B1 = a[0][0] - a[1][0];
|
|
||||||
const A2 = b[1][1] - b[0][1];
|
|
||||||
const B2 = b[0][0] - b[1][0];
|
|
||||||
const D = A1 * B2 - A2 * B1;
|
|
||||||
if (D !== 0) {
|
|
||||||
const C1 = A1 * a[0][0] + B1 * a[0][1];
|
|
||||||
const C2 = A2 * b[0][0] + B2 * b[0][1];
|
|
||||||
return pointFrom<Point>((C1 * B2 - C2 * B1) / D, (A1 * C2 - A2 * C1) / D);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function line<P extends GlobalPoint | LocalPoint>(a: P, b: P): Line<P> {
|
|
||||||
return [a, b] as Line<P>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
|
||||||
point: Point,
|
|
||||||
line: LineSegment<Point>,
|
|
||||||
threshold = PRECISION,
|
|
||||||
) => {
|
|
||||||
const distance = distanceToLineSegment(point, line);
|
|
||||||
|
|
||||||
if (distance === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return distance < threshold;
|
|
||||||
};
|
|
||||||
|
|
||||||
const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
|
|
||||||
point: Point,
|
|
||||||
line: LineSegment<Point>,
|
|
||||||
) => {
|
|
||||||
const [x, y] = point;
|
|
||||||
const [[x1, y1], [x2, y2]] = line;
|
|
||||||
|
|
||||||
const A = x - x1;
|
|
||||||
const B = y - y1;
|
|
||||||
const C = x2 - x1;
|
|
||||||
const D = y2 - y1;
|
|
||||||
|
|
||||||
const dot = A * C + B * D;
|
|
||||||
const len_sq = C * C + D * D;
|
|
||||||
let param = -1;
|
|
||||||
if (len_sq !== 0) {
|
|
||||||
param = dot / len_sq;
|
|
||||||
}
|
|
||||||
|
|
||||||
let xx;
|
|
||||||
let yy;
|
|
||||||
|
|
||||||
if (param < 0) {
|
|
||||||
xx = x1;
|
|
||||||
yy = y1;
|
|
||||||
} else if (param > 1) {
|
|
||||||
xx = x2;
|
|
||||||
yy = y2;
|
|
||||||
} else {
|
|
||||||
xx = x1 + param * C;
|
|
||||||
yy = y1 + param * D;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dx = x - xx;
|
|
||||||
const dy = y - yy;
|
|
||||||
return Math.sqrt(dx * dx + dy * dy);
|
|
||||||
};
|
|
||||||
|
|
||||||
function pointFrom<Point extends GlobalPoint | LocalPoint>(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
): Point {
|
|
||||||
return [x, y] as Point;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adapated from https://www.npmjs.com/package/points-on-curve/v/1.0.1
|
|
||||||
export function simplify(points: any, distance: any) {
|
|
||||||
return simplifyPoints(points, 0, points.length, distance, []);
|
|
||||||
}
|
|
||||||
// Ramer–Douglas–Peucker algorithm
|
|
||||||
// https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
|
|
||||||
function simplifyPoints(
|
|
||||||
points: any,
|
|
||||||
start: any,
|
|
||||||
end: any,
|
|
||||||
epsilon: any,
|
|
||||||
newPoints: any[],
|
|
||||||
) {
|
|
||||||
const outPoints: any[] = newPoints || [];
|
|
||||||
// find the most distance point from the endpoints
|
|
||||||
const s = points[start];
|
|
||||||
const e = points[end - 1];
|
|
||||||
let maxDistSq = 0;
|
|
||||||
let maxNdx = 1;
|
|
||||||
for (let i = start + 1; i < end - 1; ++i) {
|
|
||||||
const distSq = distanceToSegmentSq(points[i], s, e);
|
|
||||||
if (distSq > maxDistSq) {
|
|
||||||
maxDistSq = distSq;
|
|
||||||
maxNdx = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if that point is too far, split
|
|
||||||
if (Math.sqrt(maxDistSq) > epsilon) {
|
|
||||||
simplifyPoints(points, start, maxNdx + 1, epsilon, outPoints);
|
|
||||||
simplifyPoints(points, maxNdx, end, epsilon, outPoints);
|
|
||||||
} else {
|
|
||||||
if (!outPoints.length) {
|
|
||||||
outPoints.push(s);
|
|
||||||
}
|
|
||||||
outPoints.push(e);
|
|
||||||
}
|
|
||||||
return outPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
// distance between 2 points squared
|
|
||||||
function distanceSq(p1: any, p2: any) {
|
|
||||||
return Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2);
|
|
||||||
}
|
|
||||||
// Sistance squared from a point p to the line segment vw
|
|
||||||
function distanceToSegmentSq(p: any, v: any, w: any) {
|
|
||||||
const l2 = distanceSq(v, w);
|
|
||||||
if (l2 === 0) {
|
|
||||||
return distanceSq(p, v);
|
|
||||||
}
|
|
||||||
let t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
|
|
||||||
t = Math.max(0, Math.min(1, t));
|
|
||||||
return distanceSq(p, lerp(v, w, t));
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerp(a: any, b: any, t: any) {
|
|
||||||
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Commands, getLassoSelectedElementIds } from "./lasso-shared.chunk";
|
|
||||||
|
|
||||||
import type { LassoWorkerInput } from "./lasso-main";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Due to this export (and related dynamic import), this worker code will be included in the bundle automatically (as a separate chunk),
|
|
||||||
* without the need for esbuild / vite /rollup plugins and special browser / server treatment.
|
|
||||||
*
|
|
||||||
* `import.meta.url` is undefined in nodejs
|
|
||||||
*/
|
|
||||||
export const WorkerUrl: URL | undefined = import.meta.url
|
|
||||||
? new URL(import.meta.url)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// run only in the worker context
|
|
||||||
if (typeof window === "undefined" && typeof self !== "undefined") {
|
|
||||||
self.onmessage = (event: MessageEvent<LassoWorkerInput>) => {
|
|
||||||
switch (event.data.command) {
|
|
||||||
case Commands.GET_LASSO_SELECTED_ELEMENT_IDS:
|
|
||||||
const result = getLassoSelectedElementIds(event.data);
|
|
||||||
self.postMessage(result);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
111
packages/excalidraw/lasso/utils.ts
Normal file
111
packages/excalidraw/lasso/utils.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import { simplify } from "points-on-curve";
|
||||||
|
|
||||||
|
import {
|
||||||
|
polygonFromPoints,
|
||||||
|
polygonIncludesPoint,
|
||||||
|
lineSegment,
|
||||||
|
lineSegmentIntersectionPoints,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
|
||||||
|
|
||||||
|
export const getLassoSelectedElementIds = (input: {
|
||||||
|
lassoPath: GlobalPoint[];
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
elementsSegments: ElementsSegmentsMap;
|
||||||
|
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||||
|
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||||
|
simplifyDistance?: number;
|
||||||
|
}): {
|
||||||
|
selectedElementIds: string[];
|
||||||
|
} => {
|
||||||
|
const {
|
||||||
|
lassoPath,
|
||||||
|
elements,
|
||||||
|
elementsSegments,
|
||||||
|
intersectedElements,
|
||||||
|
enclosedElements,
|
||||||
|
simplifyDistance,
|
||||||
|
} = input;
|
||||||
|
// simplify the path to reduce the number of points
|
||||||
|
let path: GlobalPoint[] = lassoPath;
|
||||||
|
if (simplifyDistance) {
|
||||||
|
path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
|
||||||
|
}
|
||||||
|
// close the path to form a polygon for enclosure check
|
||||||
|
const closedPath = polygonFromPoints(path);
|
||||||
|
// as the path might not enclose a shape anymore, clear before checking
|
||||||
|
enclosedElements.clear();
|
||||||
|
for (const element of elements) {
|
||||||
|
if (
|
||||||
|
!intersectedElements.has(element.id) &&
|
||||||
|
!enclosedElements.has(element.id)
|
||||||
|
) {
|
||||||
|
const enclosed = enclosureTest(closedPath, element, elementsSegments);
|
||||||
|
if (enclosed) {
|
||||||
|
enclosedElements.add(element.id);
|
||||||
|
} else {
|
||||||
|
const intersects = intersectionTest(
|
||||||
|
closedPath,
|
||||||
|
element,
|
||||||
|
elementsSegments,
|
||||||
|
);
|
||||||
|
if (intersects) {
|
||||||
|
intersectedElements.add(element.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [...intersectedElements, ...enclosedElements];
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedElementIds: results,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const enclosureTest = (
|
||||||
|
lassoPath: GlobalPoint[],
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsSegments: ElementsSegmentsMap,
|
||||||
|
): boolean => {
|
||||||
|
const lassoPolygon = polygonFromPoints(lassoPath);
|
||||||
|
const segments = elementsSegments.get(element.id);
|
||||||
|
if (!segments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.some((segment) => {
|
||||||
|
return segment.some((point) => polygonIncludesPoint(point, lassoPolygon));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const intersectionTest = (
|
||||||
|
lassoPath: GlobalPoint[],
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsSegments: ElementsSegmentsMap,
|
||||||
|
): boolean => {
|
||||||
|
const elementSegments = elementsSegments.get(element.id);
|
||||||
|
if (!elementSegments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc.push(lineSegment(lassoPath[index - 1], point));
|
||||||
|
return acc;
|
||||||
|
}, [] as LineSegment<GlobalPoint>[]);
|
||||||
|
|
||||||
|
return lassoSegments.some((lassoSegment) =>
|
||||||
|
elementSegments.some(
|
||||||
|
(elementSegment) =>
|
||||||
|
// introduce a bit of tolerance to account for roughness and simplification of paths
|
||||||
|
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
|
@ -29,14 +29,11 @@ import { Excalidraw } from "../index";
|
||||||
|
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
import {
|
import { getLassoSelectedElementIds } from "../lasso/utils";
|
||||||
Commands,
|
|
||||||
getLassoSelectedElementIds,
|
|
||||||
} from "../lasso/lasso-shared.chunk";
|
|
||||||
|
|
||||||
import { act, render } from "./test-utils";
|
import { act, render } from "./test-utils";
|
||||||
|
|
||||||
import type { ElementsSegmentsMap } from "../lasso/lasso-shared.chunk";
|
import type { ElementsSegmentsMap } from "../lasso/utils";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -68,7 +65,6 @@ const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = getLassoSelectedElementIds({
|
const result = getLassoSelectedElementIds({
|
||||||
command: Commands.GET_LASSO_SELECTED_ELEMENT_IDS,
|
|
||||||
lassoPath:
|
lassoPath:
|
||||||
h.app.lassoTrail
|
h.app.lassoTrail
|
||||||
.getCurrentTrail()
|
.getCurrentTrail()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue