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