mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
fix: do not modify elements while erasing (#7531)
This commit is contained in:
parent
3ecf72a507
commit
872973f145
@ -57,7 +57,6 @@ import {
|
||||
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
DRAGGING_THRESHOLD,
|
||||
ELEMENT_READY_TO_ERASE_OPACITY,
|
||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||
ELEMENT_TRANSLATE_AMOUNT,
|
||||
ENV,
|
||||
@ -247,6 +246,7 @@ import {
|
||||
ToolType,
|
||||
OnUserFollowedPayload,
|
||||
UnsubscribeCallback,
|
||||
ElementsPendingErasure,
|
||||
} from "../types";
|
||||
import {
|
||||
debounce,
|
||||
@ -402,6 +402,7 @@ import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
||||
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
||||
import FollowMode from "./FollowMode/FollowMode";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
import { getRenderOpacity } from "../renderer/renderElement";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
@ -527,6 +528,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private iFrameRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement>();
|
||||
private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>();
|
||||
|
||||
private elementsPendingErasure: ElementsPendingErasure = new Set();
|
||||
|
||||
hitLinkElement?: NonDeletedExcalidrawElement;
|
||||
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
|
||||
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
|
||||
@ -1075,7 +1078,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}px) scale(${scale})`
|
||||
: "none",
|
||||
display: isVisible ? "block" : "none",
|
||||
opacity: el.opacity / 100,
|
||||
opacity: getRenderOpacity(
|
||||
el,
|
||||
getContainingFrame(el),
|
||||
this.elementsPendingErasure,
|
||||
),
|
||||
["--embeddable-radius" as string]: `${getCornerRadius(
|
||||
Math.min(el.width, el.height),
|
||||
el,
|
||||
@ -1583,6 +1590,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
renderGrid: true,
|
||||
canvasBackgroundColor:
|
||||
this.state.viewBackgroundColor,
|
||||
elementsPendingErasure: this.elementsPendingErasure,
|
||||
}}
|
||||
/>
|
||||
<InteractiveCanvas
|
||||
@ -5062,31 +5070,25 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState: PointerDownState,
|
||||
scenePointer: { x: number; y: number },
|
||||
) => {
|
||||
const updateElementIds = (elements: ExcalidrawElement[]) => {
|
||||
elements.forEach((element) => {
|
||||
let didChange = false;
|
||||
|
||||
const processElements = (elements: ExcalidrawElement[]) => {
|
||||
for (const element of elements) {
|
||||
if (element.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
idsToUpdate.push(element.id);
|
||||
if (event.altKey) {
|
||||
if (
|
||||
pointerDownState.elementIdsToErase[element.id] &&
|
||||
pointerDownState.elementIdsToErase[element.id].erase
|
||||
) {
|
||||
pointerDownState.elementIdsToErase[element.id].erase = false;
|
||||
if (this.elementsPendingErasure.delete(element.id)) {
|
||||
didChange = true;
|
||||
}
|
||||
} else if (!pointerDownState.elementIdsToErase[element.id]) {
|
||||
pointerDownState.elementIdsToErase[element.id] = {
|
||||
erase: true,
|
||||
opacity: element.opacity,
|
||||
};
|
||||
} else if (!this.elementsPendingErasure.has(element.id)) {
|
||||
didChange = true;
|
||||
this.elementsPendingErasure.add(element.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const idsToUpdate: Array<string> = [];
|
||||
|
||||
const distance = distance2d(
|
||||
pointerDownState.lastCoords.x,
|
||||
pointerDownState.lastCoords.y,
|
||||
@ -5098,7 +5100,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
let samplingInterval = 0;
|
||||
while (samplingInterval <= distance) {
|
||||
const hitElements = this.getElementsAtPosition(point.x, point.y);
|
||||
updateElementIds(hitElements);
|
||||
processElements(hitElements);
|
||||
|
||||
// Exit since we reached current point
|
||||
if (samplingInterval === distance) {
|
||||
@ -5117,35 +5119,31 @@ class App extends React.Component<AppProps, AppState> {
|
||||
point.y = nextY;
|
||||
}
|
||||
|
||||
const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
|
||||
const id =
|
||||
isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId)
|
||||
? ele.containerId
|
||||
: ele.id;
|
||||
if (idsToUpdate.includes(id)) {
|
||||
if (event.altKey) {
|
||||
if (
|
||||
pointerDownState.elementIdsToErase[id] &&
|
||||
pointerDownState.elementIdsToErase[id].erase === false
|
||||
) {
|
||||
return newElementWith(ele, {
|
||||
opacity: pointerDownState.elementIdsToErase[id].opacity,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return newElementWith(ele, {
|
||||
opacity: ELEMENT_READY_TO_ERASE_OPACITY,
|
||||
});
|
||||
}
|
||||
}
|
||||
return ele;
|
||||
});
|
||||
|
||||
this.scene.replaceAllElements(elements);
|
||||
|
||||
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.onSceneUpdated();
|
||||
}
|
||||
};
|
||||
|
||||
// set touch moving for mobile context menu
|
||||
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => {
|
||||
invalidateContextMenu = true;
|
||||
@ -5831,7 +5829,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
boxSelection: {
|
||||
hasOccurred: false,
|
||||
},
|
||||
elementIdsToErase: {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -7815,18 +7812,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
hitElements.forEach(
|
||||
(hitElement) =>
|
||||
(pointerDownState.elementIdsToErase[hitElement.id] = {
|
||||
erase: true,
|
||||
opacity: hitElement.opacity,
|
||||
}),
|
||||
hitElements.forEach((hitElement) =>
|
||||
this.elementsPendingErasure.add(hitElement.id),
|
||||
);
|
||||
}
|
||||
this.eraseElements(pointerDownState);
|
||||
this.eraseElements();
|
||||
return;
|
||||
} else if (Object.keys(pointerDownState.elementIdsToErase).length) {
|
||||
this.restoreReadyToEraseElements(pointerDownState);
|
||||
} else if (this.elementsPendingErasure.size) {
|
||||
this.restoreReadyToEraseElements();
|
||||
}
|
||||
|
||||
if (
|
||||
@ -8087,65 +8080,32 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
private restoreReadyToEraseElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
) => {
|
||||
const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
|
||||
if (
|
||||
pointerDownState.elementIdsToErase[ele.id] &&
|
||||
pointerDownState.elementIdsToErase[ele.id].erase
|
||||
) {
|
||||
return newElementWith(ele, {
|
||||
opacity: pointerDownState.elementIdsToErase[ele.id].opacity,
|
||||
});
|
||||
} else if (
|
||||
isBoundToContainer(ele) &&
|
||||
pointerDownState.elementIdsToErase[ele.containerId] &&
|
||||
pointerDownState.elementIdsToErase[ele.containerId].erase
|
||||
) {
|
||||
return newElementWith(ele, {
|
||||
opacity: pointerDownState.elementIdsToErase[ele.containerId].opacity,
|
||||
});
|
||||
} else if (
|
||||
ele.frameId &&
|
||||
pointerDownState.elementIdsToErase[ele.frameId] &&
|
||||
pointerDownState.elementIdsToErase[ele.frameId].erase
|
||||
) {
|
||||
return newElementWith(ele, {
|
||||
opacity: pointerDownState.elementIdsToErase[ele.frameId].opacity,
|
||||
});
|
||||
}
|
||||
return ele;
|
||||
});
|
||||
|
||||
this.scene.replaceAllElements(elements);
|
||||
private restoreReadyToEraseElements = () => {
|
||||
this.elementsPendingErasure = new Set();
|
||||
this.onSceneUpdated();
|
||||
};
|
||||
|
||||
private eraseElements = (pointerDownState: PointerDownState) => {
|
||||
private eraseElements = () => {
|
||||
let didChange = false;
|
||||
const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
|
||||
if (
|
||||
pointerDownState.elementIdsToErase[ele.id] &&
|
||||
pointerDownState.elementIdsToErase[ele.id].erase
|
||||
) {
|
||||
return newElementWith(ele, { isDeleted: true });
|
||||
} else if (
|
||||
isBoundToContainer(ele) &&
|
||||
pointerDownState.elementIdsToErase[ele.containerId] &&
|
||||
pointerDownState.elementIdsToErase[ele.containerId].erase
|
||||
) {
|
||||
return newElementWith(ele, { isDeleted: true });
|
||||
} else if (
|
||||
ele.frameId &&
|
||||
pointerDownState.elementIdsToErase[ele.frameId] &&
|
||||
pointerDownState.elementIdsToErase[ele.frameId].erase
|
||||
this.elementsPendingErasure.has(ele.id) ||
|
||||
(ele.frameId && this.elementsPendingErasure.has(ele.frameId)) ||
|
||||
(isBoundToContainer(ele) &&
|
||||
this.elementsPendingErasure.has(ele.containerId))
|
||||
) {
|
||||
didChange = true;
|
||||
return newElementWith(ele, { isDeleted: true });
|
||||
}
|
||||
return ele;
|
||||
});
|
||||
|
||||
this.history.resumeRecording();
|
||||
this.scene.replaceAllElements(elements);
|
||||
this.elementsPendingErasure = new Set();
|
||||
|
||||
if (didChange) {
|
||||
this.history.resumeRecording();
|
||||
this.scene.replaceAllElements(elements);
|
||||
}
|
||||
};
|
||||
|
||||
private initializeImage = async ({
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawFrameLikeElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
isTextElement,
|
||||
@ -36,10 +37,12 @@ import {
|
||||
BinaryFiles,
|
||||
Zoom,
|
||||
InteractiveCanvasAppState,
|
||||
ElementsPendingErasure,
|
||||
} from "../types";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import {
|
||||
BOUND_TEXT_PADDING,
|
||||
ELEMENT_READY_TO_ERASE_OPACITY,
|
||||
FRAME_STYLE,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
MIME_TYPES,
|
||||
@ -94,6 +97,27 @@ const shouldResetImageFilter = (
|
||||
const getCanvasPadding = (element: ExcalidrawElement) =>
|
||||
element.type === "freedraw" ? element.strokeWidth * 12 : 20;
|
||||
|
||||
export const getRenderOpacity = (
|
||||
element: ExcalidrawElement,
|
||||
containingFrame: ExcalidrawFrameLikeElement | null,
|
||||
elementsPendingErasure: ElementsPendingErasure,
|
||||
) => {
|
||||
// multiplying frame opacity with element opacity to combine them
|
||||
// (e.g. frame 50% and element 50% opacity should result in 25% opacity)
|
||||
let opacity = ((containingFrame?.opacity ?? 100) * element.opacity) / 10000;
|
||||
|
||||
// if pending erasure, multiply again to combine further
|
||||
// (so that erasing always results in lower opacity than original)
|
||||
if (
|
||||
elementsPendingErasure.has(element.id) ||
|
||||
(containingFrame && elementsPendingErasure.has(containingFrame.id))
|
||||
) {
|
||||
opacity *= ELEMENT_READY_TO_ERASE_OPACITY / 100;
|
||||
}
|
||||
|
||||
return opacity;
|
||||
};
|
||||
|
||||
export interface ExcalidrawElementWithCanvas {
|
||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||
canvas: HTMLCanvasElement;
|
||||
@ -269,8 +293,6 @@ const drawElementOnCanvas = (
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
context.globalAlpha =
|
||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "iframe":
|
||||
@ -372,7 +394,6 @@ const drawElementOnCanvas = (
|
||||
}
|
||||
}
|
||||
}
|
||||
context.globalAlpha = 1;
|
||||
};
|
||||
|
||||
export const elementWithCanvasCache = new WeakMap<
|
||||
@ -595,6 +616,12 @@ export const renderElement = (
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
context.globalAlpha = getRenderOpacity(
|
||||
element,
|
||||
getContainingFrame(element),
|
||||
renderConfig.elementsPendingErasure,
|
||||
);
|
||||
|
||||
switch (element.type) {
|
||||
case "magicframe":
|
||||
case "frame": {
|
||||
@ -831,6 +858,8 @@ export const renderElement = (
|
||||
throw new Error(`Unimplemented type ${element.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
context.globalAlpha = 1;
|
||||
};
|
||||
|
||||
const roughSVGDrawWithPrecision = (
|
||||
|
@ -266,6 +266,7 @@ export const exportToCanvas = async (
|
||||
imageCache,
|
||||
renderGrid: false,
|
||||
isExporting: true,
|
||||
elementsPendingErasure: new Set(),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
ElementsPendingErasure,
|
||||
InteractiveCanvasAppState,
|
||||
StaticCanvasAppState,
|
||||
} from "../types";
|
||||
@ -20,6 +21,7 @@ export type StaticCanvasRenderConfig = {
|
||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||
CSS filters), and we disable render optimizations for best output */
|
||||
isExporting: boolean;
|
||||
elementsPendingErasure: ElementsPendingErasure;
|
||||
};
|
||||
|
||||
export type SVGRenderConfig = {
|
||||
|
@ -633,12 +633,6 @@ export type PointerDownState = Readonly<{
|
||||
boxSelection: {
|
||||
hasOccurred: boolean;
|
||||
};
|
||||
elementIdsToErase: {
|
||||
[key: ExcalidrawElement["id"]]: {
|
||||
opacity: ExcalidrawElement["opacity"];
|
||||
erase: boolean;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
export type UnsubscribeCallback = () => void;
|
||||
@ -751,3 +745,5 @@ export type Primitive =
|
||||
| undefined;
|
||||
|
||||
export type JSONValue = string | number | boolean | null | object;
|
||||
|
||||
export type ElementsPendingErasure = Set<ExcalidrawElement["id"]>;
|
||||
|
Loading…
Reference in New Issue
Block a user