Forráskód Böngészése

support resizing multiple elements including texts (#1726)

Co-authored-by: David Luzar <luzar.david@gmail.com>
Daishi Kato 5 éve
szülő
commit
53ab46126d
4 módosított fájl, 192 hozzáadás és 195 törlés
  1. 24 31
      src/components/App.tsx
  2. 0 1
      src/element/index.ts
  3. 121 113
      src/element/resizeElements.ts
  4. 47 50
      src/renderer/renderScene.ts

+ 24 - 31
src/components/App.tsx

@@ -23,7 +23,6 @@ import {
   newLinearElement,
   resizeElements,
   getElementWithResizeHandler,
-  canResizeMutlipleElements,
   getResizeOffsetXY,
   getResizeArrowDirection,
   getResizeHandlerFromCoords,
@@ -1771,20 +1770,18 @@ class App extends React.Component<any, AppState> {
         return;
       }
     } else if (selectedElements.length > 1 && !isOverScrollBar) {
-      if (canResizeMutlipleElements(selectedElements)) {
-        const resizeHandle = getResizeHandlerFromCoords(
-          getCommonBounds(selectedElements),
-          scenePointerX,
-          scenePointerY,
-          this.state.zoom,
-          event.pointerType,
-        );
-        if (resizeHandle) {
-          document.documentElement.style.cursor = getCursorForResizingElement({
-            resizeHandle,
-          });
-          return;
-        }
+      const resizeHandle = getResizeHandlerFromCoords(
+        getCommonBounds(selectedElements),
+        scenePointerX,
+        scenePointerY,
+        this.state.zoom,
+        event.pointerType,
+      );
+      if (resizeHandle) {
+        document.documentElement.style.cursor = getCursorForResizingElement({
+          resizeHandle,
+        });
+        return;
       }
     }
     const hitElement = getElementAtPosition(
@@ -2054,22 +2051,18 @@ class App extends React.Component<any, AppState> {
           isResizingElements = true;
         }
       } else if (selectedElements.length > 1) {
-        if (canResizeMutlipleElements(selectedElements)) {
-          resizeHandle = getResizeHandlerFromCoords(
-            getCommonBounds(selectedElements),
-            x,
-            y,
-            this.state.zoom,
-            event.pointerType,
-          );
-          if (resizeHandle) {
-            document.documentElement.style.cursor = getCursorForResizingElement(
-              {
-                resizeHandle,
-              },
-            );
-            isResizingElements = true;
-          }
+        resizeHandle = getResizeHandlerFromCoords(
+          getCommonBounds(selectedElements),
+          x,
+          y,
+          this.state.zoom,
+          event.pointerType,
+        );
+        if (resizeHandle) {
+          document.documentElement.style.cursor = getCursorForResizingElement({
+            resizeHandle,
+          });
+          isResizingElements = true;
         }
       }
       if (isResizingElements) {

+ 0 - 1
src/element/index.ts

@@ -35,7 +35,6 @@ export {
 } from "./resizeTest";
 export {
   resizeElements,
-  canResizeMutlipleElements,
   getResizeOffsetXY,
   getResizeArrowDirection,
 } from "./resizeElements";

+ 121 - 113
src/element/resizeElements.ts

@@ -204,6 +204,37 @@ const rescalePointsInElement = (
       }
     : {};
 
+// This is not computationally ideal, but can't be helped.
+const measureFontSizeFromWH = (
+  element: NonDeleted<ExcalidrawTextElement>,
+  nextWidth: number,
+  nextHeight: number,
+): { size: number; baseline: number } | null => {
+  let scale = Math.min(nextWidth / element.width, nextHeight / element.height);
+  let nextFontSize = element.fontSize * scale;
+  let metrics = measureText(
+    element.text,
+    getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
+  );
+  if (metrics.width - nextWidth < 1 && metrics.height - nextHeight < 1) {
+    return { size: nextFontSize, baseline: metrics.baseline };
+  }
+  // second measurement
+  scale = Math.min(
+    Math.min(nextWidth, metrics.width) / element.width,
+    Math.min(nextHeight, metrics.height) / element.height,
+  );
+  nextFontSize = element.fontSize * scale;
+  metrics = measureText(
+    element.text,
+    getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
+  );
+  if (metrics.width - nextWidth < 1 && metrics.height - nextHeight < 1) {
+    return { size: nextFontSize, baseline: metrics.baseline };
+  }
+  return null;
+};
+
 const resizeSingleTextElement = (
   element: NonDeleted<ExcalidrawTextElement>,
   resizeHandle: "nw" | "ne" | "sw" | "se",
@@ -250,22 +281,16 @@ const resizeSingleTextElement = (
       break;
   }
   if (scale > 0) {
-    const newFontSize = Math.max(element.fontSize * scale, 10);
-    const metrics = measureText(
-      element.text,
-      getFontString({ fontSize: newFontSize, fontFamily: element.fontFamily }),
-    );
-    if (
-      Math.abs(metrics.width - element.width) <= 1 ||
-      Math.abs(metrics.height - element.height) <= 1
-    ) {
-      // we ignore 1px change to avoid janky behavior
+    const nextWidth = element.width * scale;
+    const nextHeight = element.height * scale;
+    const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
+    if (nextFont === null) {
       return;
     }
     const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
       element,
-      metrics.width,
-      metrics.height,
+      nextWidth,
+      nextHeight,
     );
     const deltaX1 = (x1 - nextX1) / 2;
     const deltaY1 = (y1 - nextY1) / 2;
@@ -283,10 +308,10 @@ const resizeSingleTextElement = (
       isResizeFromCenter,
     );
     mutateElement(element, {
-      fontSize: newFontSize,
-      width: metrics.width,
-      height: metrics.height,
-      baseline: metrics.baseline,
+      fontSize: nextFont.size,
+      width: nextWidth,
+      height: nextHeight,
+      baseline: nextFont.baseline,
       x: nextElementX,
       y: nextElementY,
     });
@@ -398,124 +423,107 @@ const resizeMultipleElements = (
   pointerY: number,
 ) => {
   const [x1, y1, x2, y2] = getCommonBounds(elements);
+  let scale: number;
+  let getNextXY: (
+    element: NonDeletedExcalidrawElement,
+    origCoords: readonly [number, number, number, number],
+    finalCoords: readonly [number, number, number, number],
+  ) => { x: number; y: number };
   switch (resizeHandle) {
-    case "se": {
-      const scale = Math.max(
+    case "se":
+      scale = Math.max(
         (pointerX - x1) / (x2 - x1),
         (pointerY - y1) / (y2 - y1),
       );
-      if (scale > 0) {
-        elements.forEach((element) => {
-          const width = element.width * scale;
-          const height = element.height * scale;
-          const [origX1, origY1] = getElementAbsoluteCoords(element);
-          const rescaledPoints = rescalePointsInElement(element, width, height);
-          const [finalX1, finalY1] = getResizedElementAbsoluteCoords(
-            {
-              ...element,
-              ...rescaledPoints,
-            },
-            width,
-            height,
-          );
-          const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
-          const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
-          mutateElement(element, { width, height, x, y, ...rescaledPoints });
-        });
-      }
+      getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
+        const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
+        const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
+        return { x, y };
+      };
       break;
-    }
-    case "nw": {
-      const scale = Math.max(
+    case "nw":
+      scale = Math.max(
         (x2 - pointerX) / (x2 - x1),
         (y2 - pointerY) / (y2 - y1),
       );
-      if (scale > 0) {
-        elements.forEach((element) => {
-          const width = element.width * scale;
-          const height = element.height * scale;
-          const [, , origX2, origY2] = getElementAbsoluteCoords(element);
-          const rescaledPoints = rescalePointsInElement(element, width, height);
-          const [, , finalX2, finalY2] = getResizedElementAbsoluteCoords(
-            {
-              ...element,
-              ...rescaledPoints,
-            },
-            width,
-            height,
-          );
-          const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
-          const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
-          mutateElement(element, { width, height, x, y, ...rescaledPoints });
-        });
-      }
+      getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
+        const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
+        const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
+        return { x, y };
+      };
       break;
-    }
-    case "ne": {
-      const scale = Math.max(
+    case "ne":
+      scale = Math.max(
         (pointerX - x1) / (x2 - x1),
         (y2 - pointerY) / (y2 - y1),
       );
-      if (scale > 0) {
-        elements.forEach((element) => {
-          const width = element.width * scale;
-          const height = element.height * scale;
-          const [origX1, , , origY2] = getElementAbsoluteCoords(element);
-          const rescaledPoints = rescalePointsInElement(element, width, height);
-          const [finalX1, , , finalY2] = getResizedElementAbsoluteCoords(
-            {
-              ...element,
-              ...rescaledPoints,
-            },
-            width,
-            height,
-          );
-          const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
-          const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
-          mutateElement(element, { width, height, x, y, ...rescaledPoints });
-        });
-      }
+      getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
+        const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
+        const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
+        return { x, y };
+      };
       break;
-    }
-    case "sw": {
-      const scale = Math.max(
+    case "sw":
+      scale = Math.max(
         (x2 - pointerX) / (x2 - x1),
         (pointerY - y1) / (y2 - y1),
       );
-      if (scale > 0) {
-        elements.forEach((element) => {
-          const width = element.width * scale;
-          const height = element.height * scale;
-          const [, origY1, origX2] = getElementAbsoluteCoords(element);
-          const rescaledPoints = rescalePointsInElement(element, width, height);
-          const [, finalY1, finalX2] = getResizedElementAbsoluteCoords(
-            {
-              ...element,
-              ...rescaledPoints,
-            },
-            width,
-            height,
-          );
-          const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
-          const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
-          mutateElement(element, { width, height, x, y, ...rescaledPoints });
-        });
-      }
+      getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
+        const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
+        const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
+        return { x, y };
+      };
       break;
+  }
+  if (scale > 0) {
+    const updates = elements.reduce(
+      (prev, element) => {
+        if (!prev) {
+          return prev;
+        }
+        const width = element.width * scale;
+        const height = element.height * scale;
+        let font: { fontSize?: number; baseline?: number } = {};
+        if (element.type === "text") {
+          const nextFont = measureFontSizeFromWH(element, width, height);
+          if (nextFont === null) {
+            return null;
+          }
+          font = { fontSize: nextFont.size, baseline: nextFont.baseline };
+        }
+        const origCoords = getElementAbsoluteCoords(element);
+        const rescaledPoints = rescalePointsInElement(element, width, height);
+        const finalCoords = getResizedElementAbsoluteCoords(
+          {
+            ...element,
+            ...rescaledPoints,
+          },
+          width,
+          height,
+        );
+        const { x, y } = getNextXY(element, origCoords, finalCoords);
+        return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
+      },
+      [] as
+        | {
+            width: number;
+            height: number;
+            x: number;
+            y: number;
+            points?: (readonly [number, number])[];
+            fontSize?: number;
+            baseline?: number;
+          }[]
+        | null,
+    );
+    if (updates) {
+      elements.forEach((element, index) => {
+        mutateElement(element, updates[index]);
+      });
     }
   }
 };
 
-export const canResizeMutlipleElements = (
-  elements: readonly NonDeletedExcalidrawElement[],
-) => {
-  return elements.every(
-    (element) =>
-      ["rectangle", "diamond", "ellipse"].includes(element.type) ||
-      isLinearElement(element),
-  );
-};
-
 export const getResizeOffsetXY = (
   resizeHandle: ResizeTestType,
   selectedElements: NonDeletedExcalidrawElement[],

+ 47 - 50
src/renderer/renderScene.ts

@@ -16,7 +16,6 @@ import {
   handlerRectanglesFromCoords,
   handlerRectangles,
   getCommonBounds,
-  canResizeMutlipleElements,
 } from "../element";
 
 import { roundRect } from "./roundRect";
@@ -372,55 +371,53 @@ export const renderScene = (
       });
       context.translate(-sceneState.scrollX, -sceneState.scrollY);
     } else if (locallySelectedElements.length > 1) {
-      if (canResizeMutlipleElements(locallySelectedElements)) {
-        const dashedLinePadding = 4 / sceneState.zoom;
-        context.translate(sceneState.scrollX, sceneState.scrollY);
-        context.fillStyle = oc.white;
-        const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
-        const initialLineDash = context.getLineDash();
-        context.setLineDash([2 / sceneState.zoom]);
-        const lineWidth = context.lineWidth;
-        context.lineWidth = 1 / sceneState.zoom;
-        strokeRectWithRotation(
-          context,
-          x1 - dashedLinePadding,
-          y1 - dashedLinePadding,
-          x2 - x1 + dashedLinePadding * 2,
-          y2 - y1 + dashedLinePadding * 2,
-          (x1 + x2) / 2,
-          (y1 + y2) / 2,
-          0,
-        );
-        context.lineWidth = lineWidth;
-        context.setLineDash(initialLineDash);
-        const handlers = handlerRectanglesFromCoords(
-          [x1, y1, x2, y2],
-          0,
-          sceneState.zoom,
-          undefined,
-          OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
-        );
-        Object.keys(handlers).forEach((key) => {
-          const handler = handlers[key as HandlerRectanglesRet];
-          if (handler !== undefined) {
-            const lineWidth = context.lineWidth;
-            context.lineWidth = 1 / sceneState.zoom;
-            strokeRectWithRotation(
-              context,
-              handler[0],
-              handler[1],
-              handler[2],
-              handler[3],
-              handler[0] + handler[2] / 2,
-              handler[1] + handler[3] / 2,
-              0,
-              true, // fill before stroke
-            );
-            context.lineWidth = lineWidth;
-          }
-        });
-        context.translate(-sceneState.scrollX, -sceneState.scrollY);
-      }
+      const dashedLinePadding = 4 / sceneState.zoom;
+      context.translate(sceneState.scrollX, sceneState.scrollY);
+      context.fillStyle = oc.white;
+      const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
+      const initialLineDash = context.getLineDash();
+      context.setLineDash([2 / sceneState.zoom]);
+      const lineWidth = context.lineWidth;
+      context.lineWidth = 1 / sceneState.zoom;
+      strokeRectWithRotation(
+        context,
+        x1 - dashedLinePadding,
+        y1 - dashedLinePadding,
+        x2 - x1 + dashedLinePadding * 2,
+        y2 - y1 + dashedLinePadding * 2,
+        (x1 + x2) / 2,
+        (y1 + y2) / 2,
+        0,
+      );
+      context.lineWidth = lineWidth;
+      context.setLineDash(initialLineDash);
+      const handlers = handlerRectanglesFromCoords(
+        [x1, y1, x2, y2],
+        0,
+        sceneState.zoom,
+        undefined,
+        OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
+      );
+      Object.keys(handlers).forEach((key) => {
+        const handler = handlers[key as HandlerRectanglesRet];
+        if (handler !== undefined) {
+          const lineWidth = context.lineWidth;
+          context.lineWidth = 1 / sceneState.zoom;
+          strokeRectWithRotation(
+            context,
+            handler[0],
+            handler[1],
+            handler[2],
+            handler[3],
+            handler[0] + handler[2] / 2,
+            handler[1] + handler[3] / 2,
+            0,
+            true, // fill before stroke
+          );
+          context.lineWidth = lineWidth;
+        }
+      });
+      context.translate(-sceneState.scrollX, -sceneState.scrollY);
     }
   }