瀏覽代碼

Fix many syncing issues (#952)

vanilla_orig
Pete Hunt 5 年之前
父節點
當前提交
3f8144ef85
沒有連結到貢獻者的電子郵件帳戶。

+ 2
- 0
.gitignore 查看文件

9
 yarn-debug.log*
9
 yarn-debug.log*
10
 yarn-error.log*
10
 yarn-error.log*
11
 yarn.lock
11
 yarn.lock
12
+.envrc
13
+firebase/

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

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

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

7
 import { ExcalidrawElement } from "../element/types";
7
 import { ExcalidrawElement } from "../element/types";
8
 import { AppState } from "../types";
8
 import { AppState } from "../types";
9
 import { KEYS } from "../keys";
9
 import { KEYS } from "../keys";
10
+import { getElementMap } from "../element";
11
+import { newElementWith } from "../element/mutateElement";
10
 
12
 
11
 const writeData = (
13
 const writeData = (
14
+  prevElements: readonly ExcalidrawElement[],
12
   appState: AppState,
15
   appState: AppState,
13
   updater: () => { elements: ExcalidrawElement[]; appState: AppState } | null,
16
   updater: () => { elements: ExcalidrawElement[]; appState: AppState } | null,
14
 ) => {
17
 ) => {
19
     !appState.draggingElement
22
     !appState.draggingElement
20
   ) {
23
   ) {
21
     const data = updater();
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
   return {};
52
   return {};
31
 };
53
 };
37
 
59
 
38
 export const createUndoAction: ActionCreator = history => ({
60
 export const createUndoAction: ActionCreator = history => ({
39
   name: "undo",
61
   name: "undo",
40
-  perform: (_, appState) => writeData(appState, () => history.undoOnce()),
62
+  perform: (elements, appState) =>
63
+    writeData(elements, appState, () => history.undoOnce()),
41
   keyTest: testUndo(false),
64
   keyTest: testUndo(false),
42
   PanelComponent: ({ updateData }) => (
65
   PanelComponent: ({ updateData }) => (
43
     <ToolButton
66
     <ToolButton
52
 
75
 
53
 export const createRedoAction: ActionCreator = history => ({
76
 export const createRedoAction: ActionCreator = history => ({
54
   name: "redo",
77
   name: "redo",
55
-  perform: (_, appState) => writeData(appState, () => history.redoOnce()),
78
+  perform: (elements, appState) =>
79
+    writeData(elements, appState, () => history.redoOnce()),
56
   keyTest: testUndo(true),
80
   keyTest: testUndo(true),
57
   PanelComponent: ({ updateData }) => (
81
   PanelComponent: ({ updateData }) => (
58
     <ToolButton
82
     <ToolButton

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

34
     openMenu: null,
34
     openMenu: null,
35
     lastPointerDownWith: "mouse",
35
     lastPointerDownWith: "mouse",
36
     selectedElementIds: {},
36
     selectedElementIds: {},
37
-    deletedIds: {},
38
     collaborators: new Map(),
37
     collaborators: new Map(),
39
   };
38
   };
40
 }
39
 }

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

18
   getCursorForResizingElement,
18
   getCursorForResizingElement,
19
   getPerfectElementSize,
19
   getPerfectElementSize,
20
   normalizeDimensions,
20
   normalizeDimensions,
21
+  getElementMap,
22
+  getDrawingVersion,
23
+  getSyncableElements,
24
+  hasNonDeletedElements,
21
 } from "../element";
25
 } from "../element";
22
 import {
26
 import {
23
   deleteSelectedElements,
27
   deleteSelectedElements,
160
   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initalized
164
   socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initalized
161
   roomID: string | null = null;
165
   roomID: string | null = null;
162
   roomKey: string | null = null;
166
   roomKey: string | null = null;
167
+  lastBroadcastedOrReceivedSceneVersion: number = -1;
163
 
168
 
164
   actionManager: ActionManager;
169
   actionManager: ActionManager;
165
   canvasOnlyActions = ["selectAll"];
170
   canvasOnlyActions = ["selectAll"];
275
             iv,
280
             iv,
276
           );
281
           );
277
 
282
 
278
-          let deletedIds = this.state.deletedIds;
279
           switch (decryptedData.type) {
283
           switch (decryptedData.type) {
280
             case "INVALID_RESPONSE":
284
             case "INVALID_RESPONSE":
281
               return;
285
               return;
282
             case "SCENE_UPDATE":
286
             case "SCENE_UPDATE":
283
-              const {
284
-                elements: remoteElements,
285
-                appState: remoteAppState,
286
-              } = decryptedData.payload;
287
+              const { elements: remoteElements } = decryptedData.payload;
287
               const restoredState = restore(remoteElements || [], null, {
288
               const restoredState = restore(remoteElements || [], null, {
288
                 scrollToContent: true,
289
                 scrollToContent: true,
289
               });
290
               });
295
               } else {
296
               } else {
296
                 // create a map of ids so we don't have to iterate
297
                 // create a map of ids so we don't have to iterate
297
                 // over the array more than once.
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
                 // Reconcile
301
                 // Reconcile
326
                 elements = restoredState.elements
302
                 elements = restoredState.elements
342
                     ) {
318
                     ) {
343
                       elements.push(localElementMap[element.id]);
319
                       elements.push(localElementMap[element.id]);
344
                       delete localElementMap[element.id];
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
                       } else {
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
                         elements.push(element);
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
                     return elements;
344
                     return elements;
360
                   // add local elements that weren't deleted or on remote
346
                   // add local elements that weren't deleted or on remote
361
                   .concat(...Object.values(localElementMap));
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
               if (this.socketInitialized === false) {
358
               if (this.socketInitialized === false) {
367
                 this.socketInitialized = true;
359
                 this.socketInitialized = true;
368
               }
360
               }
370
             case "MOUSE_LOCATION":
362
             case "MOUSE_LOCATION":
371
               const { socketID, pointerCoords } = decryptedData.payload;
363
               const { socketID, pointerCoords } = decryptedData.payload;
372
               this.setState(state => {
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
               break;
373
               break;
382
           }
374
           }
428
   };
420
   };
429
 
421
 
430
   private broadcastSceneUpdate = () => {
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
     const data: SocketUpdateDataSource["SCENE_UPDATE"] = {
423
     const data: SocketUpdateDataSource["SCENE_UPDATE"] = {
439
       type: "SCENE_UPDATE",
424
       type: "SCENE_UPDATE",
440
       payload: {
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
     return this._broadcastSocketData(
433
     return this._broadcastSocketData(
450
       data as typeof data & { _brand: "socketUpdateData" },
434
       data as typeof data & { _brand: "socketUpdateData" },
451
     );
435
     );
840
                       action: () => this.pasteFromClipboard(null),
824
                       action: () => this.pasteFromClipboard(null),
841
                     },
825
                     },
842
                     probablySupportsClipboardBlob &&
826
                     probablySupportsClipboardBlob &&
843
-                      elements.length > 0 && {
827
+                      hasNonDeletedElements(elements) && {
844
                         label: t("labels.copyAsPng"),
828
                         label: t("labels.copyAsPng"),
845
                         action: this.copyToClipboardAsPng,
829
                         action: this.copyToClipboardAsPng,
846
                       },
830
                       },
1102
       const pnt = points[points.length - 1];
1086
       const pnt = points[points.length - 1];
1103
       pnt[0] = x - originX;
1087
       pnt[0] = x - originX;
1104
       pnt[1] = y - originY;
1088
       pnt[1] = y - originY;
1089
+      mutateElement(multiElement);
1105
       invalidateShapeForElement(multiElement);
1090
       invalidateShapeForElement(multiElement);
1106
       this.setState({});
1091
       this.setState({});
1107
       return;
1092
       return;
1485
           },
1470
           },
1486
         }));
1471
         }));
1487
         multiElement.points.push([x - rx, y - ry]);
1472
         multiElement.points.push([x - rx, y - ry]);
1473
+        mutateElement(multiElement);
1488
         invalidateShapeForElement(multiElement);
1474
         invalidateShapeForElement(multiElement);
1489
       } else {
1475
       } else {
1490
         this.setState(prevState => ({
1476
         this.setState(prevState => ({
1494
           },
1480
           },
1495
         }));
1481
         }));
1496
         element.points.push([0, 0]);
1482
         element.points.push([0, 0]);
1483
+        mutateElement(element);
1497
         invalidateShapeForElement(element);
1484
         invalidateShapeForElement(element);
1498
         elements = [...elements, element];
1485
         elements = [...elements, element];
1499
         this.setState({
1486
         this.setState({
1548
 
1535
 
1549
         const dx = element.x + width + p1[0];
1536
         const dx = element.x + width + p1[0];
1550
         const dy = element.y + height + p1[1];
1537
         const dy = element.y + height + p1[1];
1538
+        p1[0] = absPx - element.x;
1539
+        p1[1] = absPy - element.y;
1551
         mutateElement(element, {
1540
         mutateElement(element, {
1552
           x: dx,
1541
           x: dx,
1553
           y: dy,
1542
           y: dy,
1554
         });
1543
         });
1555
-        p1[0] = absPx - element.x;
1556
-        p1[1] = absPy - element.y;
1557
       } else {
1544
       } else {
1545
+        p1[0] -= deltaX;
1546
+        p1[1] -= deltaY;
1558
         mutateElement(element, {
1547
         mutateElement(element, {
1559
           x: element.x + deltaX,
1548
           x: element.x + deltaX,
1560
           y: element.y + deltaY,
1549
           y: element.y + deltaY,
1561
         });
1550
         });
1562
-
1563
-        p1[0] -= deltaX;
1564
-        p1[1] -= deltaY;
1565
       }
1551
       }
1566
     };
1552
     };
1567
 
1553
 
1586
         p1[0] += deltaX;
1572
         p1[0] += deltaX;
1587
         p1[1] += deltaY;
1573
         p1[1] += deltaY;
1588
       }
1574
       }
1575
+      mutateElement(element);
1589
     };
1576
     };
1590
 
1577
 
1591
     const onPointerMove = (event: PointerEvent) => {
1578
     const onPointerMove = (event: PointerEvent) => {
1925
           pnt[0] = dx;
1912
           pnt[0] = dx;
1926
           pnt[1] = dy;
1913
           pnt[1] = dy;
1927
         }
1914
         }
1915
+
1916
+        mutateElement(draggingElement, { points });
1928
       } else {
1917
       } else {
1929
         if (event.shiftKey) {
1918
         if (event.shiftKey) {
1930
           ({ width, height } = getPerfectElementSize(
1919
           ({ width, height } = getPerfectElementSize(
2005
             x - draggingElement.x,
1994
             x - draggingElement.x,
2006
             y - draggingElement.y,
1995
             y - draggingElement.y,
2007
           ]);
1996
           ]);
1997
+          mutateElement(draggingElement);
2008
           invalidateShapeForElement(draggingElement);
1998
           invalidateShapeForElement(draggingElement);
2009
           this.setState({
1999
           this.setState({
2010
             multiElement: this.state.draggingElement,
2000
             multiElement: this.state.draggingElement,
2263
     if (scrollBars) {
2253
     if (scrollBars) {
2264
       currentScrollBars = scrollBars;
2254
       currentScrollBars = scrollBars;
2265
     }
2255
     }
2266
-    const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
2256
+    const scrolledOutside =
2257
+      !atLeastOneVisibleElement && hasNonDeletedElements(elements);
2267
     if (this.state.scrolledOutside !== scrolledOutside) {
2258
     if (this.state.scrolledOutside !== scrolledOutside) {
2268
       this.setState({ scrolledOutside: scrolledOutside });
2259
       this.setState({ scrolledOutside: scrolledOutside });
2269
     }
2260
     }
2270
     this.saveDebounced();
2261
     this.saveDebounced();
2271
-    if (history.isRecording()) {
2262
+
2263
+    if (
2264
+      getDrawingVersion(elements) > this.lastBroadcastedOrReceivedSceneVersion
2265
+    ) {
2272
       this.broadcastSceneUpdate();
2266
       this.broadcastSceneUpdate();
2267
+    }
2268
+
2269
+    if (history.isRecording()) {
2273
       history.pushEntry(this.state, elements);
2270
       history.pushEntry(this.state, elements);
2274
       history.skipRecording();
2271
       history.skipRecording();
2275
     }
2272
     }

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

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

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

52
 
52
 
53
       return {
53
       return {
54
         ...element,
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
         id: element.id || nanoid(),
57
         id: element.id || nanoid(),
57
         fillStyle: element.fillStyle || "hachure",
58
         fillStyle: element.fillStyle || "hachure",
58
         strokeWidth: element.strokeWidth || 1,
59
         strokeWidth: element.strokeWidth || 1,

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

1
+import { ExcalidrawElement } from "./types";
2
+import { isInvisiblySmallElement } from "./sizeHelpers";
3
+
1
 export { newElement, newTextElement, duplicateElement } from "./newElement";
4
 export { newElement, newTextElement, duplicateElement } from "./newElement";
2
 export {
5
 export {
3
   getElementAbsoluteCoords,
6
   getElementAbsoluteCoords,
24
   normalizeDimensions,
27
   normalizeDimensions,
25
 } from "./sizeHelpers";
28
 } from "./sizeHelpers";
26
 export { showSelectedShapeActions } from "./showSelectedShapeActions";
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
 import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
1
 import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
2
+import { randomSeed } from "roughjs/bin/math";
2
 
3
 
3
 type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
4
 type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
4
   Partial<TElement>,
5
   Partial<TElement>,
10
 // the same drawing.
11
 // the same drawing.
11
 export function mutateElement(
12
 export function mutateElement(
12
   element: ExcalidrawElement,
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
   (element as any).version++;
19
   (element as any).version++;
20
+  (element as any).versionNonce = randomSeed();
17
 }
21
 }
18
 
22
 
19
 export function newElementWith(
23
 export function newElementWith(
20
   element: ExcalidrawElement,
24
   element: ExcalidrawElement,
21
   updates: ElementUpdate<ExcalidrawElement>,
25
   updates: ElementUpdate<ExcalidrawElement>,
22
 ): ExcalidrawElement {
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
 // This function tracks updates of text elements for the purposes for collaboration.
35
 // This function tracks updates of text elements for the purposes for collaboration.
32
 ): void {
41
 ): void {
33
   Object.assign(element, updates);
42
   Object.assign(element, updates);
34
   (element as any).version++;
43
   (element as any).version++;
44
+  (element as any).versionNonce = randomSeed();
35
 }
45
 }
36
 
46
 
37
 export function newTextElementWith(
47
 export function newTextElementWith(
38
   element: ExcalidrawTextElement,
48
   element: ExcalidrawTextElement,
39
   updates: ElementUpdate<ExcalidrawTextElement>,
49
   updates: ElementUpdate<ExcalidrawTextElement>,
40
 ): ExcalidrawTextElement {
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
     seed: randomSeed(),
34
     seed: randomSeed(),
35
     points: [] as Point[],
35
     points: [] as Point[],
36
     version: 1,
36
     version: 1,
37
+    versionNonce: 0,
38
+    isDeleted: false,
37
   };
39
   };
38
   return element;
40
   return element;
39
 }
41
 }

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

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

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

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

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

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

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

1
 import { ExcalidrawElement } from "../element/types";
1
 import { ExcalidrawElement } from "../element/types";
2
 import { getElementAbsoluteCoords } from "../element";
2
 import { getElementAbsoluteCoords } from "../element";
3
 import { AppState } from "../types";
3
 import { AppState } from "../types";
4
+import { newElementWith } from "../element/mutateElement";
4
 
5
 
5
 export function getElementsWithinSelection(
6
 export function getElementsWithinSelection(
6
   elements: readonly ExcalidrawElement[],
7
   elements: readonly ExcalidrawElement[],
34
   elements: readonly ExcalidrawElement[],
35
   elements: readonly ExcalidrawElement[],
35
   appState: AppState,
36
   appState: AppState,
36
 ) {
37
 ) {
37
-  const deletedIds: AppState["deletedIds"] = {};
38
   return {
38
   return {
39
-    elements: elements.filter(el => {
39
+    elements: elements.map(el => {
40
       if (appState.selectedElementIds[el.id]) {
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
     appState: {
45
     appState: {
49
       ...appState,
46
       ...appState,
50
       selectedElementIds: {},
47
       selectedElementIds: {},
51
-      deletedIds: {
52
-        ...appState.deletedIds,
53
-        ...deletedIds,
54
-      },
55
     },
48
     },
56
   };
49
   };
57
 }
50
 }

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

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

Loading…
取消
儲存