Ver código fonte

Regenerate roughjs shape only when the item is updated (#316)

* Regenerate roughjs shape only when the item is updated

* Remove shape object during export and history undo/redo

* Remove shape element during copying

* Fix shape generation during creation
Gasim Gasimzada 5 anos atrás
pai
commit
74764b06eb

+ 8 - 0
src/actions/actionProperties.tsx

@@ -24,6 +24,7 @@ export const actionChangeStrokeColor: Action = {
     return {
       elements: changeProperty(elements, el => ({
         ...el,
+        shape: null,
         strokeColor: value
       })),
       appState: { ...appState, currentItemStrokeColor: value }
@@ -50,6 +51,7 @@ export const actionChangeBackgroundColor: Action = {
     return {
       elements: changeProperty(elements, el => ({
         ...el,
+        shape: null,
         backgroundColor: value
       })),
       appState: { ...appState, currentItemBackgroundColor: value }
@@ -76,6 +78,7 @@ export const actionChangeFillStyle: Action = {
     return {
       elements: changeProperty(elements, el => ({
         ...el,
+        shape: null,
         fillStyle: value
       }))
     };
@@ -104,6 +107,7 @@ export const actionChangeStrokeWidth: Action = {
     return {
       elements: changeProperty(elements, el => ({
         ...el,
+        shape: null,
         strokeWidth: value
       }))
     };
@@ -130,6 +134,7 @@ export const actionChangeSloppiness: Action = {
     return {
       elements: changeProperty(elements, el => ({
         ...el,
+        shape: null,
         roughness: value
       }))
     };
@@ -156,6 +161,7 @@ export const actionChangeOpacity: Action = {
     return {
       elements: changeProperty(elements, el => ({
         ...el,
+        shape: null,
         opacity: value
       }))
     };
@@ -185,6 +191,7 @@ export const actionChangeFontSize: Action = {
         if (isTextElement(el)) {
           const element: ExcalidrawTextElement = {
             ...el,
+            shape: null,
             font: `${value}px ${el.font.split("px ")[1]}`
           };
           redrawTextBoundingBox(element);
@@ -223,6 +230,7 @@ export const actionChangeFontFamily: Action = {
         if (isTextElement(el)) {
           const element: ExcalidrawTextElement = {
             ...el,
+            shape: null,
             font: `${el.font.split("px ")[0]}px ${value}`
           };
           redrawTextBoundingBox(element);

+ 1 - 0
src/actions/actionStyles.ts

@@ -27,6 +27,7 @@ export const actionPasteStyles: Action = {
         if (element.isSelected) {
           const newElement = {
             ...element,
+            shape: null,
             backgroundColor: pastedElement?.backgroundColor,
             strokeWidth: pastedElement?.strokeWidth,
             strokeColor: pastedElement?.strokeColor,

+ 3 - 1
src/element/newElement.ts

@@ -1,5 +1,6 @@
 import { randomSeed } from "../random";
 import nanoid from "nanoid";
+import { Drawable } from "roughjs/bin/core";
 
 export function newElement(
   type: string,
@@ -28,7 +29,8 @@ export function newElement(
     roughness,
     opacity,
     isSelected: false,
-    seed: randomSeed()
+    seed: randomSeed(),
+    shape: null as Drawable | Drawable[] | null
   };
   return element;
 }

+ 4 - 1
src/history.ts

@@ -7,7 +7,10 @@ class SceneHistory {
 
   generateCurrentEntry(elements: readonly ExcalidrawElement[]) {
     return JSON.stringify(
-      elements.map(element => ({ ...element, isSelected: false }))
+      elements.map(({ shape, ...element }) => ({
+        ...element,
+        isSelected: false
+      }))
     );
   }
 

+ 17 - 3
src/index.tsx

@@ -279,7 +279,9 @@ export class App extends React.Component<{}, AppState> {
   private copyToClipboard = () => {
     if (navigator.clipboard) {
       const text = JSON.stringify(
-        elements.filter(element => element.isSelected)
+        elements
+          .filter(element => element.isSelected)
+          .map(({ shape, ...el }) => el)
       );
       navigator.clipboard.writeText(text);
     }
@@ -303,7 +305,11 @@ export class App extends React.Component<{}, AppState> {
         onCut={e => {
           e.clipboardData.setData(
             "text/plain",
-            JSON.stringify(elements.filter(element => element.isSelected))
+            JSON.stringify(
+              elements
+                .filter(element => element.isSelected)
+                .map(({ shape, ...el }) => el)
+            )
           );
           elements = deleteSelectedElements(elements);
           this.forceUpdate();
@@ -312,7 +318,11 @@ export class App extends React.Component<{}, AppState> {
         onCopy={e => {
           e.clipboardData.setData(
             "text/plain",
-            JSON.stringify(elements.filter(element => element.isSelected))
+            JSON.stringify(
+              elements
+                .filter(element => element.isSelected)
+                .map(({ shape, ...el }) => el)
+            )
           );
           e.preventDefault();
         }}
@@ -465,6 +475,7 @@ export class App extends React.Component<{}, AppState> {
               1,
               100
             );
+
             type ResizeTestType = ReturnType<typeof resizeTest>;
             let resizeHandle: ResizeTestType = false;
             let isResizingElements = false;
@@ -670,6 +681,7 @@ export class App extends React.Component<{}, AppState> {
 
                     el.x = element.x;
                     el.y = element.y;
+                    el.shape = null;
                   });
                   lastX = x;
                   lastY = y;
@@ -705,6 +717,7 @@ export class App extends React.Component<{}, AppState> {
               // otherwise we would read a stale one!
               const draggingElement = this.state.draggingElement;
               if (!draggingElement) return;
+
               let width =
                 e.clientX -
                 CANVAS_WINDOW_OFFSET_LEFT -
@@ -720,6 +733,7 @@ export class App extends React.Component<{}, AppState> {
               draggingElement.height = e.shiftKey
                 ? Math.abs(width) * Math.sign(height)
                 : height;
+              draggingElement.shape = null;
 
               if (this.state.elementType === "selection") {
                 elements = setSelection(elements, draggingElement);

+ 72 - 62
src/renderer/renderElement.ts

@@ -4,6 +4,7 @@ import { ExcalidrawElement } from "../element/types";
 import { isTextElement } from "../element/typeChecks";
 import { getDiamondPoints, getArrowPoints } from "../element/bounds";
 import { RoughCanvas } from "roughjs/bin/canvas";
+import { Drawable } from "roughjs/bin/core";
 
 export function renderElement(
   element: ExcalidrawElement,
@@ -17,69 +18,76 @@ export function renderElement(
     context.fillRect(0, 0, element.width, element.height);
     context.fillStyle = fillStyle;
   } else if (element.type === "rectangle") {
-    const shape = withCustomMathRandom(element.seed, () => {
-      return generator.rectangle(0, 0, element.width, element.height, {
-        stroke: element.strokeColor,
-        fill: element.backgroundColor,
-        fillStyle: element.fillStyle,
-        strokeWidth: element.strokeWidth,
-        roughness: element.roughness
-      });
-    });
-
-    context.globalAlpha = element.opacity / 100;
-    rc.draw(shape);
-    context.globalAlpha = 1;
-  } else if (element.type === "diamond") {
-    const shape = withCustomMathRandom(element.seed, () => {
-      const [
-        topX,
-        topY,
-        rightX,
-        rightY,
-        bottomX,
-        bottomY,
-        leftX,
-        leftY
-      ] = getDiamondPoints(element);
-      return generator.polygon(
-        [
-          [topX, topY],
-          [rightX, rightY],
-          [bottomX, bottomY],
-          [leftX, leftY]
-        ],
-        {
+    if (!element.shape) {
+      element.shape = withCustomMathRandom(element.seed, () => {
+        return generator.rectangle(0, 0, element.width, element.height, {
           stroke: element.strokeColor,
           fill: element.backgroundColor,
           fillStyle: element.fillStyle,
           strokeWidth: element.strokeWidth,
           roughness: element.roughness
-        }
-      );
-    });
+        });
+      });
+    }
+
+    context.globalAlpha = element.opacity / 100;
+    rc.draw(element.shape as Drawable);
+    context.globalAlpha = 1;
+  } else if (element.type === "diamond") {
+    if (!element.shape) {
+      element.shape = withCustomMathRandom(element.seed, () => {
+        const [
+          topX,
+          topY,
+          rightX,
+          rightY,
+          bottomX,
+          bottomY,
+          leftX,
+          leftY
+        ] = getDiamondPoints(element);
+        return generator.polygon(
+          [
+            [topX, topY],
+            [rightX, rightY],
+            [bottomX, bottomY],
+            [leftX, leftY]
+          ],
+          {
+            stroke: element.strokeColor,
+            fill: element.backgroundColor,
+            fillStyle: element.fillStyle,
+            strokeWidth: element.strokeWidth,
+            roughness: element.roughness
+          }
+        );
+      });
+    }
+
     context.globalAlpha = element.opacity / 100;
-    rc.draw(shape);
+    rc.draw(element.shape as Drawable);
     context.globalAlpha = 1;
   } else if (element.type === "ellipse") {
-    const shape = withCustomMathRandom(element.seed, () =>
-      generator.ellipse(
-        element.width / 2,
-        element.height / 2,
-        element.width,
-        element.height,
-        {
-          stroke: element.strokeColor,
-          fill: element.backgroundColor,
-          fillStyle: element.fillStyle,
-          strokeWidth: element.strokeWidth,
-          roughness: element.roughness
-        }
-      )
-    );
+    if (!element.shape) {
+      element.shape = withCustomMathRandom(element.seed, () =>
+        generator.ellipse(
+          element.width / 2,
+          element.height / 2,
+          element.width,
+          element.height,
+          {
+            stroke: element.strokeColor,
+            fill: element.backgroundColor,
+            fillStyle: element.fillStyle,
+            strokeWidth: element.strokeWidth,
+            roughness: element.roughness
+          }
+        )
+      );
+    }
 
     context.globalAlpha = element.opacity / 100;
-    rc.draw(shape);
+    rc.draw(element.shape as Drawable);
     context.globalAlpha = 1;
   } else if (element.type === "arrow") {
     const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
@@ -89,17 +97,19 @@ export function renderElement(
       roughness: element.roughness
     };
 
-    const shapes = withCustomMathRandom(element.seed, () => [
-      //    \
-      generator.line(x3, y3, x2, y2, options),
-      // -----
-      generator.line(x1, y1, x2, y2, options),
-      //    /
-      generator.line(x4, y4, x2, y2, options)
-    ]);
+    if (!element.shape) {
+      element.shape = withCustomMathRandom(element.seed, () => [
+        //    \
+        generator.line(x3, y3, x2, y2, options),
+        // -----
+        generator.line(x1, y1, x2, y2, options),
+        //    /
+        generator.line(x4, y4, x2, y2, options)
+      ]);
+    }
 
     context.globalAlpha = element.opacity / 100;
-    shapes.forEach(shape => rc.draw(shape));
+    (element.shape as Drawable[]).forEach(shape => rc.draw(shape));
     context.globalAlpha = 1;
     return;
   } else if (isTextElement(element)) {

+ 1 - 1
src/scene/data.ts

@@ -35,7 +35,7 @@ export function saveAsJSON(
   const serialized = JSON.stringify({
     version: 1,
     source: window.location.origin,
-    elements
+    elements: elements.map(({ shape, ...el }) => el)
   });
 
   saveFile(