refactor and improve perf

This commit is contained in:
Ryan Di 2025-04-09 11:11:13 +10:00
parent 13ce3cd166
commit 2032c857fe

View file

@ -6,7 +6,6 @@ import {
pointFrom, pointFrom,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { simplify } from "points-on-curve";
import { getElementsInGroup } from "@excalidraw/element/groups"; import { getElementsInGroup } from "@excalidraw/element/groups";
import { getElementShape } from "@excalidraw/element/shapes"; import { getElementShape } from "@excalidraw/element/shapes";
@ -32,11 +31,15 @@ import type { AnimationFrameHandler } from "../animation-frame-handler";
import type App from "../components/App"; import type App from "../components/App";
// just enough to form a segment; this is sufficient for eraser
const POINTS_ON_TRAIL = 2;
export class EraserTrail extends AnimatedTrail { 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 elementsSegments: Map<string, LineSegment<GlobalPoint>[]> = new Map(); private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
private shapesCache: Map<string, GeometricShape<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, {
@ -65,9 +68,7 @@ export class EraserTrail extends AnimatedTrail {
} }
startPath(x: number, y: number): void { startPath(x: number, y: number): void {
// clear any existing trails just in case
this.endPath(); this.endPath();
super.startPath(x, y); super.startPath(x, y);
this.elementsToErase.clear(); this.elementsToErase.clear();
} }
@ -85,94 +86,102 @@ export class EraserTrail extends AnimatedTrail {
.getCurrentTrail() .getCurrentTrail()
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])); ?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1]));
eraserPath = eraserPath?.slice(eraserPath.length - 20); // for efficiency and avoid unnecessary calculations,
// take only POINTS_ON_TRAIL points to form some number of segments
eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL);
if (!eraserPath || eraserPath.length === 0) {
return [];
}
const visibleElementsMap = arrayToMap(this.app.visibleElements); const visibleElementsMap = arrayToMap(this.app.visibleElements);
if (eraserPath) { const pathSegments = eraserPath.reduce((acc, point, index) => {
const simplifiedPath = simplify( if (index === 0) {
eraserPath, return acc;
5 / this.app.state.zoom.value, }
) as GlobalPoint[]; acc.push(lineSegment(eraserPath[index - 1], point));
return acc;
}, [] as LineSegment<GlobalPoint>[]);
for (const element of this.app.visibleElements) { for (const element of this.app.visibleElements) {
if (restoreToErase && this.elementsToErase.has(element.id)) { // restore only if already added to the to-be-erased set
const intersects = eraserTest( if (restoreToErase && this.elementsToErase.has(element.id)) {
simplifiedPath, const intersects = eraserTest(
element, pathSegments,
this.elementsSegments, element,
this.shapesCache, this.segmentsCache,
visibleElementsMap, this.geometricShapesCache,
this.app, visibleElementsMap,
); this.app,
);
if (intersects) { if (intersects) {
const shallowestGroupId = element.groupIds.at(-1)!; const shallowestGroupId = element.groupIds.at(-1)!;
if (this.groupsToErase.has(shallowestGroupId)) { if (this.groupsToErase.has(shallowestGroupId)) {
const elementsInGroup = getElementsInGroup( const elementsInGroup = getElementsInGroup(
this.app.scene.getNonDeletedElementsMap(), this.app.scene.getNonDeletedElementsMap(),
shallowestGroupId, shallowestGroupId,
); );
for (const elementInGroup of elementsInGroup) { for (const elementInGroup of elementsInGroup) {
this.elementsToErase.delete(elementInGroup.id); this.elementsToErase.delete(elementInGroup.id);
}
this.groupsToErase.delete(shallowestGroupId);
} }
this.groupsToErase.delete(shallowestGroupId);
if (isBoundToContainer(element)) {
this.elementsToErase.delete(element.containerId);
}
if (hasBoundTextElement(element)) {
const boundText = getBoundTextElementId(element);
if (boundText) {
this.elementsToErase.delete(boundText);
}
}
this.elementsToErase.delete(element.id);
} }
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
const intersects = eraserTest(
simplifiedPath,
element,
this.elementsSegments,
this.shapesCache,
visibleElementsMap,
this.app,
);
if (intersects) { if (isBoundToContainer(element)) {
const shallowestGroupId = element.groupIds.at(-1)!; this.elementsToErase.delete(element.containerId);
if (!this.groupsToErase.has(shallowestGroupId)) {
const elementsInGroup = getElementsInGroup(
this.app.scene.getNonDeletedElementsMap(),
shallowestGroupId,
);
for (const elementInGroup of elementsInGroup) {
this.elementsToErase.add(elementInGroup.id);
}
this.groupsToErase.add(shallowestGroupId);
}
if (hasBoundTextElement(element)) {
const boundText = getBoundTextElementId(element);
if (boundText) {
this.elementsToErase.add(boundText);
}
}
if (isBoundToContainer(element)) {
this.elementsToErase.add(element.containerId);
}
this.elementsToErase.add(element.id);
} }
if (hasBoundTextElement(element)) {
const boundText = getBoundTextElementId(element);
if (boundText) {
this.elementsToErase.delete(boundText);
}
}
this.elementsToErase.delete(element.id);
}
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
const intersects = eraserTest(
pathSegments,
element,
this.segmentsCache,
this.geometricShapesCache,
visibleElementsMap,
this.app,
);
if (intersects) {
const shallowestGroupId = element.groupIds.at(-1)!;
if (!this.groupsToErase.has(shallowestGroupId)) {
const elementsInGroup = getElementsInGroup(
this.app.scene.getNonDeletedElementsMap(),
shallowestGroupId,
);
for (const elementInGroup of elementsInGroup) {
this.elementsToErase.add(elementInGroup.id);
}
this.groupsToErase.add(shallowestGroupId);
}
if (hasBoundTextElement(element)) {
const boundText = getBoundTextElementId(element);
if (boundText) {
this.elementsToErase.add(boundText);
}
}
if (isBoundToContainer(element)) {
this.elementsToErase.add(element.containerId);
}
this.elementsToErase.add(element.id);
} }
} }
} }
@ -185,12 +194,12 @@ export class EraserTrail extends AnimatedTrail {
super.clearTrails(); super.clearTrails();
this.elementsToErase.clear(); this.elementsToErase.clear();
this.groupsToErase.clear(); this.groupsToErase.clear();
this.elementsSegments.clear(); this.segmentsCache.clear();
} }
} }
const eraserTest = ( const eraserTest = (
path: GlobalPoint[], pathSegments: LineSegment<GlobalPoint>[],
element: ExcalidrawElement, element: ExcalidrawElement,
elementsSegments: ElementsSegmentsMap, elementsSegments: ElementsSegmentsMap,
shapesCache = new Map<string, GeometricShape<GlobalPoint>>(), shapesCache = new Map<string, GeometricShape<GlobalPoint>>(),
@ -204,7 +213,7 @@ const eraserTest = (
shapesCache.set(element.id, shape); shapesCache.set(element.id, shape);
} }
const lastPoint = path[path.length - 1]; const lastPoint = pathSegments[pathSegments.length - 1][1];
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) { if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
return true; return true;
} }
@ -216,14 +225,6 @@ const eraserTest = (
elementsSegments.set(element.id, elementSegments); elementsSegments.set(element.id, elementSegments);
} }
const pathSegments = path.reduce((acc, point, index) => {
if (index === 0) {
return acc;
}
acc.push(lineSegment(path[index - 1], point));
return acc;
}, [] as LineSegment<GlobalPoint>[]);
return pathSegments.some((pathSegment) => return pathSegments.some((pathSegment) =>
elementSegments?.some( elementSegments?.some(
(elementSegment) => (elementSegment) =>