perf: make eraser great again (#9352)

* perf: make eraser great again

* lint

* refactor and improve perf

* lint
This commit is contained in:
Ryan Di 2025-04-16 00:58:45 +10:00 committed by GitHub
parent 6fe7de8020
commit 58f7d33d80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 259 additions and 120 deletions

View file

@ -454,7 +454,6 @@ import {
import { Emitter } from "../emitter";
import { ElementCanvasButtons } from "../components/ElementCanvasButtons";
import { Store, CaptureUpdateAction } from "../store";
import { AnimatedTrail } from "../animated-trail";
import { LaserTrails } from "../laser-trails";
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
import { textWysiwyg } from "../wysiwyg/textWysiwyg";
@ -464,6 +463,8 @@ import { isMaybeMermaidDefinition } from "../mermaid";
import { LassoTrail } from "../lasso";
import { EraserTrail } from "../eraser";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import BraveMeasureTextError from "./BraveMeasureTextError";
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
@ -675,26 +676,7 @@ class App extends React.Component<AppProps, AppState> {
animationFrameHandler = new AnimationFrameHandler();
laserTrails = new LaserTrails(this.animationFrameHandler, this);
eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, {
streamline: 0.2,
size: 5,
keepHead: true,
sizeMapping: (c) => {
const DECAY_TIME = 200;
const DECAY_LENGTH = 10;
const t = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME);
const l =
(DECAY_LENGTH -
Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
DECAY_LENGTH;
return Math.min(easeOut(l), easeOut(t));
},
fill: () =>
this.state.theme === THEME.LIGHT
? "rgba(0, 0, 0, 0.2)"
: "rgba(255, 255, 255, 0.2)",
});
eraserTrail = new EraserTrail(this.animationFrameHandler, this);
lassoTrail = new LassoTrail(this.animationFrameHandler, this);
onChangeEmitter = new Emitter<
@ -1676,8 +1658,8 @@ class App extends React.Component<AppProps, AppState> {
<SVGLayer
trails={[
this.laserTrails,
this.eraserTrail,
this.lassoTrail,
this.eraserTrail,
]}
/>
{selectedElements.length === 1 &&
@ -5163,7 +5145,7 @@ class App extends React.Component<AppProps, AppState> {
return elements;
}
private getElementHitThreshold() {
getElementHitThreshold() {
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
}
@ -6219,101 +6201,16 @@ class App extends React.Component<AppProps, AppState> {
private handleEraser = (
event: PointerEvent,
pointerDownState: PointerDownState,
scenePointer: { x: number; y: number },
) => {
this.eraserTrail.addPointToPath(scenePointer.x, scenePointer.y);
let didChange = false;
const processedGroups = new Set<ExcalidrawElement["id"]>();
const nonDeletedElements = this.scene.getNonDeletedElements();
const processElements = (elements: ExcalidrawElement[]) => {
for (const element of elements) {
if (element.locked) {
return;
}
if (event.altKey) {
if (this.elementsPendingErasure.delete(element.id)) {
didChange = true;
}
} else if (!this.elementsPendingErasure.has(element.id)) {
didChange = true;
this.elementsPendingErasure.add(element.id);
}
// (un)erase groups atomically
if (didChange && element.groupIds?.length) {
const shallowestGroupId = element.groupIds.at(-1)!;
if (!processedGroups.has(shallowestGroupId)) {
processedGroups.add(shallowestGroupId);
const elems = getElementsInGroup(
nonDeletedElements,
shallowestGroupId,
);
for (const elem of elems) {
if (event.altKey) {
this.elementsPendingErasure.delete(elem.id);
} else {
this.elementsPendingErasure.add(elem.id);
}
}
}
}
}
};
const distance = pointDistance(
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
pointFrom(scenePointer.x, scenePointer.y),
const elementsToErase = this.eraserTrail.addPointToPath(
scenePointer.x,
scenePointer.y,
event.altKey,
);
const threshold = this.getElementHitThreshold();
const p = { ...pointerDownState.lastCoords };
let samplingInterval = 0;
while (samplingInterval <= distance) {
const hitElements = this.getElementsAtPosition(p.x, p.y);
processElements(hitElements);
// Exit since we reached current point
if (samplingInterval === distance) {
break;
}
// Calculate next point in the line at a distance of sampling interval
samplingInterval = Math.min(samplingInterval + threshold, distance);
const distanceRatio = samplingInterval / distance;
const nextX = (1 - distanceRatio) * p.x + distanceRatio * scenePointer.x;
const nextY = (1 - distanceRatio) * p.y + distanceRatio * scenePointer.y;
p.x = nextX;
p.y = nextY;
}
pointerDownState.lastCoords.x = scenePointer.x;
pointerDownState.lastCoords.y = scenePointer.y;
if (didChange) {
for (const element of this.scene.getNonDeletedElements()) {
if (
isBoundToContainer(element) &&
(this.elementsPendingErasure.has(element.id) ||
this.elementsPendingErasure.has(element.containerId))
) {
if (event.altKey) {
this.elementsPendingErasure.delete(element.id);
this.elementsPendingErasure.delete(element.containerId);
} else {
this.elementsPendingErasure.add(element.id);
this.elementsPendingErasure.add(element.containerId);
}
}
}
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
this.triggerRender();
}
this.elementsPendingErasure = new Set(elementsToErase);
this.triggerRender();
};
// set touch moving for mobile context menu
@ -8159,7 +8056,7 @@ class App extends React.Component<AppProps, AppState> {
}
if (isEraserActive(this.state)) {
this.handleEraser(event, pointerDownState, pointerCoords);
this.handleEraser(event, pointerCoords);
return;
}