Browse Source

enable version bumping for collaboration

idlewinn 5 years ago
parent
commit
1419f17175

+ 7 - 4
src/actions/actionFinalize.tsx

@@ -7,6 +7,7 @@ import { done } from "../components/icons";
 import { t } from "../i18n";
 import { register } from "./register";
 import { invalidateShapeForElement } from "../renderer/renderElement";
+import { mutateElement } from "../element/mutateElement";
 
 export const actionFinalize = register({
   name: "finalize",
@@ -18,10 +19,12 @@ export const actionFinalize = register({
     if (appState.multiElement) {
       // pen and mouse have hover
       if (appState.lastPointerDownWith !== "touch") {
-        appState.multiElement.points = appState.multiElement.points.slice(
-          0,
-          appState.multiElement.points.length - 1,
-        );
+        mutateElement(appState.multiElement, multiElement => {
+          multiElement.points = multiElement.points.slice(
+            0,
+            multiElement.points.length - 1,
+          );
+        });
       }
       if (isInvisiblySmallElement(appState.multiElement)) {
         newElements = newElements.slice(0, -1);

+ 5 - 2
src/actions/actionStyles.ts

@@ -6,6 +6,7 @@ import {
 import { KEYS } from "../keys";
 import { DEFAULT_FONT } from "../appState";
 import { register } from "./register";
+import { mutateTextElement } from "../element/mutateElement";
 
 let copiedStyles: string = "{}";
 
@@ -44,8 +45,10 @@ export const actionPasteStyles = register({
             roughness: pastedElement?.roughness,
           };
           if (isTextElement(newElement)) {
-            newElement.font = pastedElement?.font || DEFAULT_FONT;
-            redrawTextBoundingBox(newElement);
+            mutateTextElement(newElement, newElement => {
+              newElement.font = pastedElement?.font || DEFAULT_FONT;
+              redrawTextBoundingBox(newElement);
+            });
           }
           return newElement;
         }

+ 127 - 71
src/components/App.tsx

@@ -88,6 +88,7 @@ import { LayerUI } from "./LayerUI";
 import { ScrollBars } from "../scene/types";
 import { invalidateShapeForElement } from "../renderer/renderElement";
 import { generateCollaborationLink, getCollaborationLinkData } from "../data";
+import { mutateElement } from "../element/mutateElement";
 
 // -----------------------------------------------------------------------------
 // TEST HOOKS
@@ -262,7 +263,29 @@ export class App extends React.Component<any, AppState> {
                 sceneAppState || getDefaultAppState(),
                 { scrollToContent: true },
               );
-              elements = restoredState.elements;
+              if (elements == null || elements.length === 0) {
+                elements = restoredState.elements;
+              } else {
+                const elementMap = elements.reduce(
+                  (
+                    acc: { [key: string]: ExcalidrawElement },
+                    element: ExcalidrawElement,
+                  ) => {
+                    acc[element.id] = element;
+                    return acc;
+                  },
+                  {},
+                );
+                elements = restoredState.elements.map(element => {
+                  if (
+                    elementMap.hasOwnProperty(element.id) &&
+                    elementMap[element.id].version > element.version
+                  ) {
+                    return elementMap[element.id];
+                  }
+                  return element;
+                });
+              }
               this.setState({});
               if (this.socketInitialized === false) {
                 this.socketInitialized = true;
@@ -774,8 +797,10 @@ export class App extends React.Component<any, AppState> {
       textY = centerElementYInViewport;
 
       // x and y will change after calling newTextElement function
-      element.x = centerElementX;
-      element.y = centerElementY;
+      mutateElement(element, element => {
+        element.x = centerElementX;
+        element.y = centerElementY;
+      });
     } else if (!event.altKey) {
       const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
         x,
@@ -783,8 +808,10 @@ export class App extends React.Component<any, AppState> {
       );
 
       if (snappedToCenterPosition) {
-        element.x = snappedToCenterPosition.elementCenterX;
-        element.y = snappedToCenterPosition.elementCenterY;
+        mutateElement(element, element => {
+          element.x = snappedToCenterPosition.elementCenterX;
+          element.y = snappedToCenterPosition.elementCenterY;
+        });
         textX = snappedToCenterPosition.wysiwygX;
         textY = snappedToCenterPosition.wysiwygY;
       }
@@ -1336,13 +1363,17 @@ export class App extends React.Component<any, AppState> {
 
         const dx = element.x + width + p1[0];
         const dy = element.y + height + p1[1];
-        element.x = dx;
-        element.y = dy;
+        mutateElement(element, element => {
+          element.x = dx;
+          element.y = dy;
+        });
         p1[0] = absPx - element.x;
         p1[1] = absPy - element.y;
       } else {
-        element.x += deltaX;
-        element.y += deltaY;
+        mutateElement(element, element => {
+          element.x += deltaX;
+          element.y += deltaY;
+        });
         p1[0] -= deltaX;
         p1[1] -= deltaY;
       }
@@ -1452,16 +1483,17 @@ export class App extends React.Component<any, AppState> {
                   event.shiftKey,
                 );
               } else {
-                element.width -= deltaX;
-                element.x += deltaX;
-
-                if (event.shiftKey) {
-                  element.y += element.height - element.width;
-                  element.height = element.width;
-                } else {
-                  element.height -= deltaY;
-                  element.y += deltaY;
-                }
+                mutateElement(element, element => {
+                  element.width -= deltaX;
+                  element.x += deltaX;
+                  if (event.shiftKey) {
+                    element.y += element.height - element.width;
+                    element.height = element.width;
+                  } else {
+                    element.height -= deltaY;
+                    element.y += deltaY;
+                  }
+                });
               }
               break;
             case "ne":
@@ -1484,14 +1516,16 @@ export class App extends React.Component<any, AppState> {
                   event.shiftKey,
                 );
               } else {
-                element.width += deltaX;
-                if (event.shiftKey) {
-                  element.y += element.height - element.width;
-                  element.height = element.width;
-                } else {
-                  element.height -= deltaY;
-                  element.y += deltaY;
-                }
+                mutateElement(element, element => {
+                  element.width += deltaX;
+                  if (event.shiftKey) {
+                    element.y += element.height - element.width;
+                    element.height = element.width;
+                  } else {
+                    element.height -= deltaY;
+                    element.y += deltaY;
+                  }
+                });
               }
               break;
             case "sw":
@@ -1514,13 +1548,15 @@ export class App extends React.Component<any, AppState> {
                   event.shiftKey,
                 );
               } else {
-                element.width -= deltaX;
-                element.x += deltaX;
-                if (event.shiftKey) {
-                  element.height = element.width;
-                } else {
-                  element.height += deltaY;
-                }
+                mutateElement(element, element => {
+                  element.width -= deltaX;
+                  element.x += deltaX;
+                  if (event.shiftKey) {
+                    element.height = element.width;
+                  } else {
+                    element.height += deltaY;
+                  }
+                });
               }
               break;
             case "se":
@@ -1543,18 +1579,22 @@ export class App extends React.Component<any, AppState> {
                   event.shiftKey,
                 );
               } else {
-                if (event.shiftKey) {
-                  element.width += deltaX;
-                  element.height = element.width;
-                } else {
-                  element.width += deltaX;
-                  element.height += deltaY;
-                }
+                mutateElement(element, element => {
+                  if (event.shiftKey) {
+                    element.width += deltaX;
+                    element.height = element.width;
+                  } else {
+                    element.width += deltaX;
+                    element.height += deltaY;
+                  }
+                });
               }
               break;
             case "n": {
-              element.height -= deltaY;
-              element.y += deltaY;
+              mutateElement(element, element => {
+                element.height -= deltaY;
+                element.y += deltaY;
+              });
 
               if (element.points.length > 0) {
                 const len = element.points.length;
@@ -1569,8 +1609,10 @@ export class App extends React.Component<any, AppState> {
               break;
             }
             case "w": {
-              element.width -= deltaX;
-              element.x += deltaX;
+              mutateElement(element, element => {
+                element.width -= deltaX;
+                element.x += deltaX;
+              });
 
               if (element.points.length > 0) {
                 const len = element.points.length;
@@ -1584,29 +1626,37 @@ export class App extends React.Component<any, AppState> {
               break;
             }
             case "s": {
-              element.height += deltaY;
-              if (element.points.length > 0) {
-                const len = element.points.length;
-                const points = [...element.points].sort((a, b) => a[1] - b[1]);
-
-                for (let i = 1; i < points.length; ++i) {
-                  const pnt = points[i];
-                  pnt[1] += deltaY / (len - i);
+              mutateElement(element, element => {
+                element.height += deltaY;
+                if (element.points.length > 0) {
+                  const len = element.points.length;
+                  const points = [...element.points].sort(
+                    (a, b) => a[1] - b[1],
+                  );
+
+                  for (let i = 1; i < points.length; ++i) {
+                    const pnt = points[i];
+                    pnt[1] += deltaY / (len - i);
+                  }
                 }
-              }
+              });
               break;
             }
             case "e": {
-              element.width += deltaX;
-              if (element.points.length > 0) {
-                const len = element.points.length;
-                const points = [...element.points].sort((a, b) => a[0] - b[0]);
-
-                for (let i = 1; i < points.length; ++i) {
-                  const pnt = points[i];
-                  pnt[0] += deltaX / (len - i);
+              mutateElement(element, element => {
+                element.width += deltaX;
+                if (element.points.length > 0) {
+                  const len = element.points.length;
+                  const points = [...element.points].sort(
+                    (a, b) => a[0] - b[0],
+                  );
+
+                  for (let i = 1; i < points.length; ++i) {
+                    const pnt = points[i];
+                    pnt[0] += deltaX / (len - i);
+                  }
                 }
-              }
+              });
               break;
             }
           }
@@ -1620,8 +1670,10 @@ export class App extends React.Component<any, AppState> {
             element,
             resizeHandle,
           });
-          el.x = element.x;
-          el.y = element.y;
+          mutateElement(el, el => {
+            el.x = element.x;
+            el.y = element.y;
+          });
           invalidateShapeForElement(el);
 
           lastX = x;
@@ -1644,8 +1696,10 @@ export class App extends React.Component<any, AppState> {
           );
 
           selectedElements.forEach(element => {
-            element.x += x - lastX;
-            element.y += y - lastY;
+            mutateElement(element, element => {
+              element.x += x - lastX;
+              element.y += y - lastY;
+            });
           });
           lastX = x;
           lastY = y;
@@ -1707,11 +1761,13 @@ export class App extends React.Component<any, AppState> {
           }
         }
 
-        draggingElement.x = x < originX ? originX - width : originX;
-        draggingElement.y = y < originY ? originY - height : originY;
+        mutateElement(draggingElement, draggingElement => {
+          draggingElement.x = x < originX ? originX - width : originX;
+          draggingElement.y = y < originY ? originY - height : originY;
 
-        draggingElement.width = width;
-        draggingElement.height = height;
+          draggingElement.width = width;
+          draggingElement.height = height;
+        });
       }
 
       invalidateShapeForElement(draggingElement);

+ 20 - 0
src/element/mutateElement.ts

@@ -0,0 +1,20 @@
+import {
+  MutableExcalidrawElement,
+  MutableExcalidrawTextElement,
+} from "./types";
+
+export function mutateElement(
+  element: MutableExcalidrawElement,
+  callback: (mutatableElement: MutableExcalidrawElement) => void,
+): void {
+  element.version++;
+  callback(element);
+}
+
+export function mutateTextElement(
+  element: MutableExcalidrawTextElement,
+  callback: (mutatableElement: MutableExcalidrawTextElement) => void,
+): void {
+  element.version++;
+  callback(element);
+}

+ 1 - 0
src/element/newElement.ts

@@ -33,6 +33,7 @@ export function newElement(
     opacity,
     seed: randomSeed(),
     points: [] as Point[],
+    version: 1,
   };
   return element;
 }

+ 11 - 6
src/element/sizeHelpers.ts

@@ -1,5 +1,6 @@
-import { ExcalidrawElement } from "./types";
+import { ExcalidrawElement, MutableExcalidrawElement } from "./types";
 import { invalidateShapeForElement } from "../renderer/renderElement";
+import { mutateElement } from "./mutateElement";
 
 export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
   if (element.type === "arrow" || element.type === "line") {
@@ -35,7 +36,7 @@ export function getPerfectElementSize(
 }
 
 export function resizePerfectLineForNWHandler(
-  element: ExcalidrawElement,
+  element: MutableExcalidrawElement,
   x: number,
   y: number,
 ) {
@@ -78,13 +79,17 @@ export function normalizeDimensions(
   }
 
   if (element.width < 0) {
-    element.width = Math.abs(element.width);
-    element.x -= element.width;
+    mutateElement(element, element => {
+      element.width = Math.abs(element.width);
+      element.x -= element.width;
+    });
   }
 
   if (element.height < 0) {
-    element.height = Math.abs(element.height);
-    element.y -= element.height;
+    mutateElement(element, element => {
+      element.height = Math.abs(element.height);
+      element.y -= element.height;
+    });
   }
 
   invalidateShapeForElement(element);

+ 4 - 2
src/element/textElement.ts

@@ -1,7 +1,9 @@
 import { measureText } from "../utils";
-import { ExcalidrawTextElement } from "./types";
+import { MutableExcalidrawTextElement } from "./types";
 
-export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
+export const redrawTextBoundingBox = (
+  element: MutableExcalidrawTextElement,
+) => {
   const metrics = measureText(element.text, element.font);
   element.width = metrics.width;
   element.height = metrics.height;

+ 6 - 2
src/element/types.ts

@@ -5,8 +5,10 @@ import { newElement } from "./newElement";
  * no computed data. The list of all ExcalidrawElements should be shareable
  * between peers and contain no state local to the peer.
  */
-export type ExcalidrawElement = ReturnType<typeof newElement>;
-export type ExcalidrawTextElement = ExcalidrawElement & {
+export type ExcalidrawElement = Readonly<ReturnType<typeof newElement>>;
+export type MutableExcalidrawElement = ReturnType<typeof newElement>;
+
+export type MutableExcalidrawTextElement = MutableExcalidrawElement & {
   type: "text";
   font: string;
   text: string;
@@ -15,4 +17,6 @@ export type ExcalidrawTextElement = ExcalidrawElement & {
   baseline: number;
 };
 
+export type ExcalidrawTextElement = Readonly<MutableExcalidrawTextElement>;
+
 export type PointerType = "mouse" | "pen" | "touch";