浏览代码

Fix many syncing issues (#952)

vanilla_orig
Pete Hunt 5 年前
父节点
当前提交
3f8144ef85
没有帐户链接到提交者的电子邮件

+ 2
- 0
.gitignore 查看文件

@@ -9,3 +9,5 @@ static
9 9
 yarn-debug.log*
10 10
 yarn-error.log*
11 11
 yarn.lock
12
+.envrc
13
+firebase/

+ 5
- 2
src/actions/actionCanvas.tsx 查看文件

@@ -9,6 +9,7 @@ import { KEYS } from "../keys";
9 9
 import { getShortcutKey } from "../utils";
10 10
 import useIsMobile from "../is-mobile";
11 11
 import { register } from "./register";
12
+import { newElementWith } from "../element/mutateElement";
12 13
 
13 14
 export const actionChangeViewBackgroundColor = register({
14 15
   name: "changeViewBackgroundColor",
@@ -33,9 +34,11 @@ export const actionChangeViewBackgroundColor = register({
33 34
 export const actionClearCanvas = register({
34 35
   name: "clearCanvas",
35 36
   commitToHistory: () => true,
36
-  perform: () => {
37
+  perform: elements => {
37 38
     return {
38
-      elements: [],
39
+      elements: elements.map(element =>
40
+        newElementWith(element, { isDeleted: true }),
41
+      ),
39 42
       appState: getDefaultAppState(),
40 43
     };
41 44
   },

+ 32
- 8
src/actions/actionHistory.tsx 查看文件

@@ -7,8 +7,11 @@ import { SceneHistory } from "../history";
7 7
 import { ExcalidrawElement } from "../element/types";
8 8
 import { AppState } from "../types";
9 9
 import { KEYS } from "../keys";
10
+import { getElementMap } from "../element";
11
+import { newElementWith } from "../element/mutateElement";
10 12
 
11 13
 const writeData = (
14
+  prevElements: readonly ExcalidrawElement[],
12 15
   appState: AppState,
13 16
   updater: () => { elements: ExcalidrawElement[]; appState: AppState } | null,
14 17
 ) => {
@@ -19,13 +22,32 @@ const writeData = (
19 22
     !appState.draggingElement
20 23
   ) {
21 24
     const data = updater();
25
+    if (data === null) {
26
+      return {};
27
+    }
22 28
 
23
-    return data === null
24
-      ? {}
25
-      : {
26
-          elements: data.elements,
27
-          appState: { ...appState, ...data.appState },
28
-        };
29
+    const prevElementMap = getElementMap(prevElements);
30
+    const nextElements = data.elements;
31
+    const nextElementMap = getElementMap(nextElements);
32
+    return {
33
+      elements: nextElements
34
+        .map(nextElement =>
35
+          newElementWith(
36
+            prevElementMap[nextElement.id] || nextElement,
37
+            nextElement,
38
+          ),
39
+        )
40
+        .concat(
41
+          prevElements
42
+            .filter(
43
+              prevElement => !nextElementMap.hasOwnProperty(prevElement.id),
44
+            )
45
+            .map(prevElement =>
46
+              newElementWith(prevElement, { isDeleted: true }),
47
+            ),
48
+        ),
49
+      appState: { ...appState, ...data.appState },
50
+    };
29 51
   }
30 52
   return {};
31 53
 };
@@ -37,7 +59,8 @@ type ActionCreator = (history: SceneHistory) => Action;
37 59
 
38 60
 export const createUndoAction: ActionCreator = history => ({
39 61
   name: "undo",
40
-  perform: (_, appState) => writeData(appState, () => history.undoOnce()),
62
+  perform: (elements, appState) =>
63
+    writeData(elements, appState, () => history.undoOnce()),
41 64
   keyTest: testUndo(false),
42 65
   PanelComponent: ({ updateData }) => (
43 66
     <ToolButton
@@ -52,7 +75,8 @@ export const createUndoAction: ActionCreator = history => ({
52 75
 
53 76
 export const createRedoAction: ActionCreator = history => ({
54 77
   name: "redo",
55
-  perform: (_, appState) => writeData(appState, () => history.redoOnce()),
78
+  perform: (elements, appState) =>
79
+    writeData(elements, appState, () => history.redoOnce()),
56 80
   keyTest: testUndo(true),
57 81
   PanelComponent: ({ updateData }) => (
58 82
     <ToolButton

+ 0
- 1
src/appState.ts 查看文件

@@ -34,7 +34,6 @@ export function getDefaultAppState(): AppState {
34 34
     openMenu: null,
35 35
     lastPointerDownWith: "mouse",
36 36
     selectedElementIds: {},
37
-    deletedIds: {},
38 37
     collaborators: new Map(),
39 38
   };
40 39
 }

+ 66
- 69
src/components/App.tsx 查看文件

@@ -18,6 +18,10 @@ import {
18 18
   getCursorForResizingElement,
19 19
   getPerfectElementSize,
20 20
   normalizeDimensions,
21
+  getElementMap,
22
+  getDrawingVersion,
23
+  getSyncableElements,
24
+  hasNonDeletedElements,
21 25
 } from "../element";
22 26
 import {
23 27
   deleteSelectedElements,
@@ -160,6 +164,7 @@ export class App extends React.Component<any, AppState> {
160 164
   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initalized
161 165
   roomID: string | null = null;
162 166
   roomKey: string | null = null;
167
+  lastBroadcastedOrReceivedSceneVersion: number = -1;
163 168
 
164 169
   actionManager: ActionManager;
165 170
   canvasOnlyActions = ["selectAll"];
@@ -275,15 +280,11 @@ export class App extends React.Component<any, AppState> {
275 280
             iv,
276 281
           );
277 282
 
278
-          let deletedIds = this.state.deletedIds;
279 283
           switch (decryptedData.type) {
280 284
             case "INVALID_RESPONSE":
281 285
               return;
282 286
             case "SCENE_UPDATE":
283
-              const {
284
-                elements: remoteElements,
285
-                appState: remoteAppState,
286
-              } = decryptedData.payload;
287
+              const { elements: remoteElements } = decryptedData.payload;
287 288
               const restoredState = restore(remoteElements || [], null, {
288 289
                 scrollToContent: true,
289 290
               });
@@ -295,32 +296,7 @@ export class App extends React.Component<any, AppState> {
295 296
               } else {
296 297
                 // create a map of ids so we don't have to iterate
297 298
                 // over the array more than once.
298
-                const localElementMap = elements.reduce(
299
-                  (
300
-                    acc: { [key: string]: ExcalidrawElement },
301
-                    element: ExcalidrawElement,
302
-                  ) => {
303
-                    acc[element.id] = element;
304
-                    return acc;
305
-                  },
306
-                  {},
307
-                );
308
-
309
-                deletedIds = { ...deletedIds };
310
-
311
-                for (const [id, remoteDeletedEl] of Object.entries(
312
-                  remoteAppState.deletedIds,
313
-                )) {
314
-                  if (
315
-                    !localElementMap[id] ||
316
-                    // don't remove local element if it's newer than the one
317
-                    //  deleted on remote
318
-                    remoteDeletedEl.version >= localElementMap[id].version
319
-                  ) {
320
-                    deletedIds[id] = remoteDeletedEl;
321
-                    delete localElementMap[id];
322
-                  }
323
-                }
299
+                const localElementMap = getElementMap(elements);
324 300
 
325 301
                 // Reconcile
326 302
                 elements = restoredState.elements
@@ -342,17 +318,27 @@ export class App extends React.Component<any, AppState> {
342 318
                     ) {
343 319
                       elements.push(localElementMap[element.id]);
344 320
                       delete localElementMap[element.id];
345
-                    } else {
346
-                      if (deletedIds.hasOwnProperty(element.id)) {
347
-                        if (element.version > deletedIds[element.id].version) {
348
-                          elements.push(element);
349
-                          delete deletedIds[element.id];
350
-                          delete localElementMap[element.id];
351
-                        }
321
+                    } else if (
322
+                      localElementMap.hasOwnProperty(element.id) &&
323
+                      localElementMap[element.id].version === element.version &&
324
+                      localElementMap[element.id].versionNonce !==
325
+                        element.versionNonce
326
+                    ) {
327
+                      // resolve conflicting edits deterministically by taking the one with the lowest versionNonce
328
+                      if (
329
+                        localElementMap[element.id].versionNonce <
330
+                        element.versionNonce
331
+                      ) {
332
+                        elements.push(localElementMap[element.id]);
352 333
                       } else {
334
+                        // it should be highly unlikely that the two versionNonces are the same. if we are
335
+                        // really worried about this, we can replace the versionNonce with the socket id.
353 336
                         elements.push(element);
354
-                        delete localElementMap[element.id];
355 337
                       }
338
+                      delete localElementMap[element.id];
339
+                    } else {
340
+                      elements.push(element);
341
+                      delete localElementMap[element.id];
356 342
                     }
357 343
 
358 344
                     return elements;
@@ -360,9 +346,15 @@ export class App extends React.Component<any, AppState> {
360 346
                   // add local elements that weren't deleted or on remote
361 347
                   .concat(...Object.values(localElementMap));
362 348
               }
363
-              this.setState({
364
-                deletedIds,
365
-              });
349
+              this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion(
350
+                elements,
351
+              );
352
+              // We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
353
+              // when we receive any messages from another peer. This UX can be pretty rough -- if you
354
+              // undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
355
+              // right now we think this is the right tradeoff.
356
+              history.clear();
357
+              this.setState({});
366 358
               if (this.socketInitialized === false) {
367 359
                 this.socketInitialized = true;
368 360
               }
@@ -370,13 +362,13 @@ export class App extends React.Component<any, AppState> {
370 362
             case "MOUSE_LOCATION":
371 363
               const { socketID, pointerCoords } = decryptedData.payload;
372 364
               this.setState(state => {
373
-                if (state.collaborators.has(socketID)) {
374
-                  const user = state.collaborators.get(socketID)!;
375
-                  user.pointer = pointerCoords;
376
-                  state.collaborators.set(socketID, user);
377
-                  return state;
365
+                if (!state.collaborators.has(socketID)) {
366
+                  state.collaborators.set(socketID, {});
378 367
                 }
379
-                return null;
368
+                const user = state.collaborators.get(socketID)!;
369
+                user.pointer = pointerCoords;
370
+                state.collaborators.set(socketID, user);
371
+                return state;
380 372
               });
381 373
               break;
382 374
           }
@@ -428,24 +420,16 @@ export class App extends React.Component<any, AppState> {
428 420
   };
429 421
 
430 422
   private broadcastSceneUpdate = () => {
431
-    const deletedIds = { ...this.state.deletedIds };
432
-    const _elements = elements.filter(element => {
433
-      if (element.id in deletedIds) {
434
-        delete deletedIds[element.id];
435
-      }
436
-      return element.id !== this.state.editingElement?.id;
437
-    });
438 423
     const data: SocketUpdateDataSource["SCENE_UPDATE"] = {
439 424
       type: "SCENE_UPDATE",
440 425
       payload: {
441
-        elements: _elements,
442
-        appState: {
443
-          viewBackgroundColor: this.state.viewBackgroundColor,
444
-          name: this.state.name,
445
-          deletedIds,
446
-        },
426
+        elements: getSyncableElements(elements),
447 427
       },
448 428
     };
429
+    this.lastBroadcastedOrReceivedSceneVersion = Math.max(
430
+      this.lastBroadcastedOrReceivedSceneVersion,
431
+      getDrawingVersion(elements),
432
+    );
449 433
     return this._broadcastSocketData(
450 434
       data as typeof data & { _brand: "socketUpdateData" },
451 435
     );
@@ -840,7 +824,7 @@ export class App extends React.Component<any, AppState> {
840 824
                       action: () => this.pasteFromClipboard(null),
841 825
                     },
842 826
                     probablySupportsClipboardBlob &&
843
-                      elements.length > 0 && {
827
+                      hasNonDeletedElements(elements) && {
844 828
                         label: t("labels.copyAsPng"),
845 829
                         action: this.copyToClipboardAsPng,
846 830
                       },
@@ -1102,6 +1086,7 @@ export class App extends React.Component<any, AppState> {
1102 1086
       const pnt = points[points.length - 1];
1103 1087
       pnt[0] = x - originX;
1104 1088
       pnt[1] = y - originY;
1089
+      mutateElement(multiElement);
1105 1090
       invalidateShapeForElement(multiElement);
1106 1091
       this.setState({});
1107 1092
       return;
@@ -1485,6 +1470,7 @@ export class App extends React.Component<any, AppState> {
1485 1470
           },
1486 1471
         }));
1487 1472
         multiElement.points.push([x - rx, y - ry]);
1473
+        mutateElement(multiElement);
1488 1474
         invalidateShapeForElement(multiElement);
1489 1475
       } else {
1490 1476
         this.setState(prevState => ({
@@ -1494,6 +1480,7 @@ export class App extends React.Component<any, AppState> {
1494 1480
           },
1495 1481
         }));
1496 1482
         element.points.push([0, 0]);
1483
+        mutateElement(element);
1497 1484
         invalidateShapeForElement(element);
1498 1485
         elements = [...elements, element];
1499 1486
         this.setState({
@@ -1548,20 +1535,19 @@ export class App extends React.Component<any, AppState> {
1548 1535
 
1549 1536
         const dx = element.x + width + p1[0];
1550 1537
         const dy = element.y + height + p1[1];
1538
+        p1[0] = absPx - element.x;
1539
+        p1[1] = absPy - element.y;
1551 1540
         mutateElement(element, {
1552 1541
           x: dx,
1553 1542
           y: dy,
1554 1543
         });
1555
-        p1[0] = absPx - element.x;
1556
-        p1[1] = absPy - element.y;
1557 1544
       } else {
1545
+        p1[0] -= deltaX;
1546
+        p1[1] -= deltaY;
1558 1547
         mutateElement(element, {
1559 1548
           x: element.x + deltaX,
1560 1549
           y: element.y + deltaY,
1561 1550
         });
1562
-
1563
-        p1[0] -= deltaX;
1564
-        p1[1] -= deltaY;
1565 1551
       }
1566 1552
     };
1567 1553
 
@@ -1586,6 +1572,7 @@ export class App extends React.Component<any, AppState> {
1586 1572
         p1[0] += deltaX;
1587 1573
         p1[1] += deltaY;
1588 1574
       }
1575
+      mutateElement(element);
1589 1576
     };
1590 1577
 
1591 1578
     const onPointerMove = (event: PointerEvent) => {
@@ -1925,6 +1912,8 @@ export class App extends React.Component<any, AppState> {
1925 1912
           pnt[0] = dx;
1926 1913
           pnt[1] = dy;
1927 1914
         }
1915
+
1916
+        mutateElement(draggingElement, { points });
1928 1917
       } else {
1929 1918
         if (event.shiftKey) {
1930 1919
           ({ width, height } = getPerfectElementSize(
@@ -2005,6 +1994,7 @@ export class App extends React.Component<any, AppState> {
2005 1994
             x - draggingElement.x,
2006 1995
             y - draggingElement.y,
2007 1996
           ]);
1997
+          mutateElement(draggingElement);
2008 1998
           invalidateShapeForElement(draggingElement);
2009 1999
           this.setState({
2010 2000
             multiElement: this.state.draggingElement,
@@ -2263,13 +2253,20 @@ export class App extends React.Component<any, AppState> {
2263 2253
     if (scrollBars) {
2264 2254
       currentScrollBars = scrollBars;
2265 2255
     }
2266
-    const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
2256
+    const scrolledOutside =
2257
+      !atLeastOneVisibleElement && hasNonDeletedElements(elements);
2267 2258
     if (this.state.scrolledOutside !== scrolledOutside) {
2268 2259
       this.setState({ scrolledOutside: scrolledOutside });
2269 2260
     }
2270 2261
     this.saveDebounced();
2271
-    if (history.isRecording()) {
2262
+
2263
+    if (
2264
+      getDrawingVersion(elements) > this.lastBroadcastedOrReceivedSceneVersion
2265
+    ) {
2272 2266
       this.broadcastSceneUpdate();
2267
+    }
2268
+
2269
+    if (history.isRecording()) {
2273 2270
       history.pushEntry(this.state, elements);
2274 2271
       history.skipRecording();
2275 2272
     }

+ 2
- 2
src/data/index.ts 查看文件

@@ -13,6 +13,7 @@ import { serializeAsJSON } from "./json";
13 13
 import { ExportType } from "../scene/types";
14 14
 import { restore } from "./restore";
15 15
 import { restoreFromLocalStorage } from "./localStorage";
16
+import { hasNonDeletedElements } from "../element";
16 17
 
17 18
 export { loadFromBlob } from "./blob";
18 19
 export { saveAsJSON, loadFromJSON } from "./json";
@@ -35,7 +36,6 @@ export type SocketUpdateDataSource = {
35 36
     type: "SCENE_UPDATE";
36 37
     payload: {
37 38
       elements: readonly ExcalidrawElement[];
38
-      appState: Pick<AppState, "viewBackgroundColor" | "name" | "deletedIds">;
39 39
     };
40 40
   };
41 41
   MOUSE_LOCATION: {
@@ -288,7 +288,7 @@ export async function exportCanvas(
288 288
     scale?: number;
289 289
   },
290 290
 ) {
291
-  if (!elements.length) {
291
+  if (!hasNonDeletedElements(elements)) {
292 292
     return window.alert(t("alerts.cannotExportEmptyCanvas"));
293 293
   }
294 294
   // calculate smallest area to fit the contents in

+ 2
- 1
src/data/restore.ts 查看文件

@@ -52,7 +52,8 @@ export function restore(
52 52
 
53 53
       return {
54 54
         ...element,
55
-        version: element.version || 0,
55
+        // all elements must have version > 0 so getDrawingVersion() will pick up newly added elements
56
+        version: element.version || 1,
56 57
         id: element.id || nanoid(),
57 58
         fillStyle: element.fillStyle || "hachure",
58 59
         strokeWidth: element.strokeWidth || 1,

+ 28
- 0
src/element/index.ts 查看文件

@@ -1,3 +1,6 @@
1
+import { ExcalidrawElement } from "./types";
2
+import { isInvisiblySmallElement } from "./sizeHelpers";
3
+
1 4
 export { newElement, newTextElement, duplicateElement } from "./newElement";
2 5
 export {
3 6
   getElementAbsoluteCoords,
@@ -24,3 +27,28 @@ export {
24 27
   normalizeDimensions,
25 28
 } from "./sizeHelpers";
26 29
 export { showSelectedShapeActions } from "./showSelectedShapeActions";
30
+
31
+export function getSyncableElements(elements: readonly ExcalidrawElement[]) {
32
+  // There are places in Excalidraw where synthetic invisibly small elements are added and removed.
33
+  // It's probably best to keep those local otherwise there might be a race condition that
34
+  // gets the app into an invalid state. I've never seen it happen but I'm worried about it :)
35
+  return elements.filter(el => !isInvisiblySmallElement(el));
36
+}
37
+
38
+export function getElementMap(elements: readonly ExcalidrawElement[]) {
39
+  return getSyncableElements(elements).reduce(
40
+    (acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
41
+      acc[element.id] = element;
42
+      return acc;
43
+    },
44
+    {},
45
+  );
46
+}
47
+
48
+export function getDrawingVersion(elements: readonly ExcalidrawElement[]) {
49
+  return elements.reduce((acc, el) => acc + el.version, 0);
50
+}
51
+
52
+export function hasNonDeletedElements(elements: readonly ExcalidrawElement[]) {
53
+  return elements.some(element => !element.isDeleted);
54
+}

+ 19
- 4
src/element/mutateElement.ts 查看文件

@@ -1,4 +1,5 @@
1 1
 import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
2
+import { randomSeed } from "roughjs/bin/math";
2 3
 
3 4
 type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
4 5
   Partial<TElement>,
@@ -10,17 +11,25 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
10 11
 // the same drawing.
11 12
 export function mutateElement(
12 13
   element: ExcalidrawElement,
13
-  updates: ElementUpdate<ExcalidrawElement>,
14
+  updates?: ElementUpdate<ExcalidrawElement>,
14 15
 ) {
15
-  Object.assign(element, updates);
16
+  if (updates) {
17
+    Object.assign(element, updates);
18
+  }
16 19
   (element as any).version++;
20
+  (element as any).versionNonce = randomSeed();
17 21
 }
18 22
 
19 23
 export function newElementWith(
20 24
   element: ExcalidrawElement,
21 25
   updates: ElementUpdate<ExcalidrawElement>,
22 26
 ): ExcalidrawElement {
23
-  return { ...element, ...updates, version: element.version + 1 };
27
+  return {
28
+    ...element,
29
+    ...updates,
30
+    version: element.version + 1,
31
+    versionNonce: randomSeed(),
32
+  };
24 33
 }
25 34
 
26 35
 // This function tracks updates of text elements for the purposes for collaboration.
@@ -32,11 +41,17 @@ export function mutateTextElement(
32 41
 ): void {
33 42
   Object.assign(element, updates);
34 43
   (element as any).version++;
44
+  (element as any).versionNonce = randomSeed();
35 45
 }
36 46
 
37 47
 export function newTextElementWith(
38 48
   element: ExcalidrawTextElement,
39 49
   updates: ElementUpdate<ExcalidrawTextElement>,
40 50
 ): ExcalidrawTextElement {
41
-  return { ...element, ...updates, version: element.version + 1 };
51
+  return {
52
+    ...element,
53
+    ...updates,
54
+    version: element.version + 1,
55
+    versionNonce: randomSeed(),
56
+  };
42 57
 }

+ 2
- 0
src/element/newElement.ts 查看文件

@@ -34,6 +34,8 @@ export function newElement(
34 34
     seed: randomSeed(),
35 35
     points: [] as Point[],
36 36
     version: 1,
37
+    versionNonce: 0,
38
+    isDeleted: false,
37 39
   };
38 40
   return element;
39 41
 }

+ 5
- 0
src/history.ts 查看文件

@@ -13,6 +13,11 @@ export class SceneHistory {
13 13
   private stateHistory: string[] = [];
14 14
   private redoStack: string[] = [];
15 15
 
16
+  clear() {
17
+    this.stateHistory.length = 0;
18
+    this.redoStack.length = 0;
19
+  }
20
+
16 21
   private generateEntry(
17 22
     appState: AppState,
18 23
     elements: readonly ExcalidrawElement[],

+ 3
- 1
src/renderer/renderScene.ts 查看文件

@@ -24,7 +24,7 @@ function colorForClientId(clientId: string) {
24 24
 }
25 25
 
26 26
 export function renderScene(
27
-  elements: readonly ExcalidrawElement[],
27
+  allElements: readonly ExcalidrawElement[],
28 28
   appState: AppState,
29 29
   selectionElement: ExcalidrawElement | null,
30 30
   scale: number,
@@ -49,6 +49,8 @@ export function renderScene(
49 49
     return { atLeastOneVisibleElement: false };
50 50
   }
51 51
 
52
+  const elements = allElements.filter(element => !element.isDeleted);
53
+
52 54
   const context = canvas.getContext("2d")!;
53 55
 
54 56
   // When doing calculations based on canvas width we should used normalized one

+ 6
- 0
src/scene/comparisons.ts 查看文件

@@ -25,6 +25,9 @@ export function getElementAtPosition(
25 25
   let hitElement = null;
26 26
   // We need to to hit testing from front (end of the array) to back (beginning of the array)
27 27
   for (let i = elements.length - 1; i >= 0; --i) {
28
+    if (elements[i].isDeleted) {
29
+      continue;
30
+    }
28 31
     if (hitTest(elements[i], appState, x, y, zoom)) {
29 32
       hitElement = elements[i];
30 33
       break;
@@ -42,6 +45,9 @@ export function getElementContainingPosition(
42 45
   let hitElement = null;
43 46
   // We need to to hit testing from front (end of the array) to back (beginning of the array)
44 47
   for (let i = elements.length - 1; i >= 0; --i) {
48
+    if (elements[i].isDeleted) {
49
+      continue;
50
+    }
45 51
     const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[i]);
46 52
     if (x1 < x && x < x2 && y1 < y && y < y2) {
47 53
       hitElement = elements[i];

+ 4
- 11
src/scene/selection.ts 查看文件

@@ -1,6 +1,7 @@
1 1
 import { ExcalidrawElement } from "../element/types";
2 2
 import { getElementAbsoluteCoords } from "../element";
3 3
 import { AppState } from "../types";
4
+import { newElementWith } from "../element/mutateElement";
4 5
 
5 6
 export function getElementsWithinSelection(
6 7
   elements: readonly ExcalidrawElement[],
@@ -34,24 +35,16 @@ export function deleteSelectedElements(
34 35
   elements: readonly ExcalidrawElement[],
35 36
   appState: AppState,
36 37
 ) {
37
-  const deletedIds: AppState["deletedIds"] = {};
38 38
   return {
39
-    elements: elements.filter(el => {
39
+    elements: elements.map(el => {
40 40
       if (appState.selectedElementIds[el.id]) {
41
-        deletedIds[el.id] = {
42
-          version: el.version,
43
-        };
44
-        return false;
41
+        return newElementWith(el, { isDeleted: true });
45 42
       }
46
-      return true;
43
+      return el;
47 44
     }),
48 45
     appState: {
49 46
       ...appState,
50 47
       selectedElementIds: {},
51
-      deletedIds: {
52
-        ...appState.deletedIds,
53
-        ...deletedIds,
54
-      },
55 48
     },
56 49
   };
57 50
 }

+ 0
- 1
src/types.ts 查看文件

@@ -34,7 +34,6 @@ export type AppState = {
34 34
   openMenu: "canvas" | "shape" | null;
35 35
   lastPointerDownWith: PointerType;
36 36
   selectedElementIds: { [id: string]: boolean };
37
-  deletedIds: { [id: string]: { version: ExcalidrawElement["version"] } };
38 37
   collaborators: Map<string, { pointer?: { x: number; y: number } }>;
39 38
 };
40 39
 

正在加载...
取消
保存