Browse Source

Resize (#103)

* Resize

* Detect collision with squares

* Disable resize for text, arrow and multiple selection

* Hide middle handlers when small
Paulo Menezes 5 years ago
parent
commit
dee8a73d3d
1 changed files with 234 additions and 25 deletions
  1. 234 25
      src/index.tsx

+ 234 - 25
src/index.tsx

@@ -170,6 +170,39 @@ function hitTest(element: ExcalidrawElement, x: number, y: number): boolean {
   }
   }
 }
 }
 
 
+function resizeTest(
+  element: ExcalidrawElement,
+  x: number,
+  y: number,
+  sceneState: SceneState
+): string | false {
+  if (element.type === "text" || element.type === "arrow") return false;
+
+  const x1 = getElementAbsoluteX1(element);
+  const x2 = getElementAbsoluteX2(element);
+  const y1 = getElementAbsoluteY1(element);
+  const y2 = getElementAbsoluteY2(element);
+
+  const handlers = handlerRectangles(x1, x2, y1, y2, sceneState);
+
+  const filter = Object.keys(handlers).filter(key => {
+    const handler = handlers[key];
+
+    return (
+      x + sceneState.scrollX >= handler[0] &&
+      x + sceneState.scrollX <= handler[0] + handler[2] &&
+      y + sceneState.scrollY >= handler[1] &&
+      y + sceneState.scrollY <= handler[1] + handler[3]
+    );
+  });
+
+  if (filter.length > 0) {
+    return filter[0];
+  }
+
+  return false;
+}
+
 function newElement(
 function newElement(
   type: string,
   type: string,
   x: number,
   x: number,
@@ -243,6 +276,77 @@ function getScrollbars(
   };
   };
 }
 }
 
 
+function handlerRectangles(
+  elementX1: number,
+  elementX2: number,
+  elementY1: number,
+  elementY2: number,
+  sceneState: SceneState
+) {
+  const margin = 4;
+  const minimumSize = 40;
+  const handlers: { [handler: string]: number[] } = {};
+
+  if (elementX2 - elementX1 > minimumSize) {
+    handlers["n"] = [
+      elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
+      elementY1 - margin + sceneState.scrollY - 8,
+      8,
+      8
+    ];
+
+    handlers["s"] = [
+      elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4,
+      elementY2 - margin + sceneState.scrollY + 8,
+      8,
+      8
+    ];
+  }
+
+  if (elementY2 - elementY1 > minimumSize) {
+    handlers["w"] = [
+      elementX1 - margin + sceneState.scrollX - 8,
+      elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
+      8,
+      8
+    ];
+
+    handlers["e"] = [
+      elementX2 - margin + sceneState.scrollX + 8,
+      elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4,
+      8,
+      8
+    ];
+  }
+
+  handlers["nw"] = [
+    elementX1 - margin + sceneState.scrollX - 8,
+    elementY1 - margin + sceneState.scrollY - 8,
+    8,
+    8
+  ]; // nw
+  handlers["ne"] = [
+    elementX2 - margin + sceneState.scrollX + 8,
+    elementY1 - margin + sceneState.scrollY - 8,
+    8,
+    8
+  ]; // ne
+  handlers["sw"] = [
+    elementX1 - margin + sceneState.scrollX - 8,
+    elementY2 - margin + sceneState.scrollY + 8,
+    8,
+    8
+  ]; // sw
+  handlers["se"] = [
+    elementX2 - margin + sceneState.scrollX + 8,
+    elementY2 - margin + sceneState.scrollY + 8,
+    8,
+    8
+  ]; // se
+
+  return handlers;
+}
+
 function renderScene(
 function renderScene(
   rc: RoughCanvas,
   rc: RoughCanvas,
   context: CanvasRenderingContext2D,
   context: CanvasRenderingContext2D,
@@ -259,6 +363,8 @@ function renderScene(
   }
   }
   context.fillStyle = fillStyle;
   context.fillStyle = fillStyle;
 
 
+  const selectedIndices = getSelectedIndices();
+
   elements.forEach(element => {
   elements.forEach(element => {
     element.draw(rc, context, sceneState);
     element.draw(rc, context, sceneState);
     if (element.isSelected) {
     if (element.isSelected) {
@@ -277,6 +383,23 @@ function renderScene(
         elementY2 - elementY1 + margin * 2
         elementY2 - elementY1 + margin * 2
       );
       );
       context.setLineDash(lineDash);
       context.setLineDash(lineDash);
+
+      if (
+        element.type !== "text" &&
+        element.type !== "arrow" &&
+        selectedIndices.length === 1
+      ) {
+        const handlers = handlerRectangles(
+          elementX1,
+          elementX2,
+          elementY1,
+          elementY2,
+          sceneState
+        );
+        Object.values(handlers).forEach(handler => {
+          context.strokeRect(handler[0], handler[1], handler[2], handler[3]);
+        });
+      }
     }
     }
   });
   });
 
 
@@ -595,6 +718,7 @@ function restore() {
 
 
 type AppState = {
 type AppState = {
   draggingElement: ExcalidrawElement | null;
   draggingElement: ExcalidrawElement | null;
+  resizingElement: ExcalidrawElement | null;
   elementType: string;
   elementType: string;
   exportBackground: boolean;
   exportBackground: boolean;
   exportVisibleOnly: boolean;
   exportVisibleOnly: boolean;
@@ -693,6 +817,7 @@ class App extends React.Component<{}, AppState> {
 
 
   public state: AppState = {
   public state: AppState = {
     draggingElement: null,
     draggingElement: null,
+    resizingElement: null,
     elementType: "selection",
     elementType: "selection",
     exportBackground: false,
     exportBackground: false,
     exportVisibleOnly: true,
     exportVisibleOnly: true,
@@ -1018,40 +1143,65 @@ class App extends React.Component<{}, AppState> {
               this.state.currentItemStrokeColor,
               this.state.currentItemStrokeColor,
               this.state.currentItemBackgroundColor
               this.state.currentItemBackgroundColor
             );
             );
+            let resizeHandle: string | false = false;
             let isDraggingElements = false;
             let isDraggingElements = false;
+            let isResizingElements = false;
             const cursorStyle = document.documentElement.style.cursor;
             const cursorStyle = document.documentElement.style.cursor;
             if (this.state.elementType === "selection") {
             if (this.state.elementType === "selection") {
-              let hitElement = null;
-              // We need to to hit testing from front (end of the array) to back (beginning of the array)
-              for (let i = elements.length - 1; i >= 0; --i) {
-                if (hitTest(elements[i], x, y)) {
-                  hitElement = elements[i];
-                  break;
+              const resizeElement = elements.find(element => {
+                return resizeTest(element, x, y, {
+                  scrollX: this.state.scrollX,
+                  scrollY: this.state.scrollY,
+                  viewBackgroundColor: this.state.viewBackgroundColor
+                });
+              });
+
+              this.setState({
+                resizingElement: resizeElement ? resizeElement : null
+              });
+
+              if (resizeElement) {
+                resizeHandle = resizeTest(resizeElement, x, y, {
+                  scrollX: this.state.scrollX,
+                  scrollY: this.state.scrollY,
+                  viewBackgroundColor: this.state.viewBackgroundColor
+                });
+                document.documentElement.style.cursor = `${resizeHandle}-resize`;
+                isResizingElements = true;
+              } else {
+                let hitElement = null;
+                
+                // We need to to hit testing from front (end of the array) to back (beginning of the array)
+                for (let i = elements.length - 1; i >= 0; --i) {
+                  if (hitTest(elements[i], x, y)) {
+                    hitElement = elements[i];
+                    break;
+                  }
                 }
                 }
-              }
 
 
-              // If we click on something
-              if (hitElement) {
-                if (hitElement.isSelected) {
-                  // If that element is not already selected, do nothing,
-                  // we're likely going to drag it
-                } else {
-                  // We unselect every other elements unless shift is pressed
-                  if (!e.shiftKey) {
-                    clearSelection();
+                // If we click on something
+                if (hitElement) {
+                  if (hitElement.isSelected) {
+                    // If that element is not already selected, do nothing,
+                    // we're likely going to drag it
+                  } else {
+                    // We unselect every other elements unless shift is pressed
+                    if (!e.shiftKey) {
+                      clearSelection();
+                    }
+                    // No matter what, we select it
+                    hitElement.isSelected = true;
                   }
                   }
-                  // No matter what, we select it
-                  hitElement.isSelected = true;
+                } else {
+                  // If we don't click on anything, let's remove all the selected elements
+                  clearSelection();
                 }
                 }
-              } else {
-                // If we don't click on anything, let's remove all the selected elements
-                clearSelection();
-              }
 
 
-              isDraggingElements = someElementIsSelected();
+                isDraggingElements = someElementIsSelected();
 
 
-              if (isDraggingElements) {
-                document.documentElement.style.cursor = "move";
+                if (isDraggingElements) {
+                  document.documentElement.style.cursor = "move";
+                }
               }
               }
             }
             }
 
 
@@ -1100,6 +1250,65 @@ class App extends React.Component<{}, AppState> {
                 return;
                 return;
               }
               }
 
 
+              if (isResizingElements && this.state.resizingElement) {
+                const el = this.state.resizingElement;
+                const selectedElements = elements.filter(el => el.isSelected);
+                if (selectedElements.length === 1) {
+                  const x = e.clientX - target.offsetLeft - this.state.scrollX;
+                  const y = e.clientY - target.offsetTop - this.state.scrollY;
+                  selectedElements.forEach(element => {
+                    switch (resizeHandle) {
+                      case "nw":
+                        element.width += element.x - lastX;
+                        element.height += element.y - lastY;
+                        element.x = lastX;
+                        element.y = lastY;
+                        break;
+                      case "ne":
+                        element.width = lastX - element.x;
+                        element.height += element.y - lastY;
+                        element.y = lastY;
+                        break;
+                      case "sw":
+                        element.width += element.x - lastX;
+                        element.x = lastX;
+                        element.height = lastY - element.y;
+                        break;
+                      case "se":
+                        element.width += x - lastX;
+                        if (e.shiftKey) {
+                          element.height = element.width;
+                        } else {
+                          element.height += y - lastY;
+                        }
+                        break;
+                      case "n":
+                        element.height += element.y - lastY;
+                        element.y = lastY;
+                        break;
+                      case "w":
+                        element.width += element.x - lastX;
+                        element.x = lastX;
+                        break;
+                      case "s":
+                        element.height = lastY - element.y;
+                        break;
+                      case "e":
+                        element.width = lastX - element.x;
+                        break;
+                    }
+
+                    el.x = element.x;
+                    el.y = element.y;
+                    generateDraw(el);
+                  });
+                  lastX = x;
+                  lastY = y;
+                  this.forceUpdate();
+                  return;
+                }
+              }
+
               if (isDraggingElements) {
               if (isDraggingElements) {
                 const selectedElements = elements.filter(el => el.isSelected);
                 const selectedElements = elements.filter(el => el.isSelected);
                 if (selectedElements.length) {
                 if (selectedElements.length) {