Jelajahi Sumber

change dimension for multiple elements

Ryan Di 1 tahun lalu
induk
melakukan
f0c1e9707a

+ 11 - 10
packages/excalidraw/components/Stats/Angle.tsx

@@ -20,17 +20,18 @@ const Angle = ({ element }: AngleProps) => {
     shouldChangeByStepSize,
     nextValue,
   ) => {
-    if (nextValue !== undefined) {
-      const nextAngle = degreeToRadian(nextValue);
-      mutateElement(element, {
-        angle: nextAngle,
-      });
-      return;
-    }
+    const _stateAtStart = stateAtStart[0];
+    if (_stateAtStart) {
+      if (nextValue !== undefined) {
+        const nextAngle = degreeToRadian(nextValue);
+        mutateElement(element, {
+          angle: nextAngle,
+        });
+        return;
+      }
 
-    if (stateAtStart) {
       const originalAngleInDegrees =
-        Math.round(radianToDegree(stateAtStart.angle) * 100) / 100;
+        Math.round(radianToDegree(_stateAtStart.angle) * 100) / 100;
       const changeInDegrees = Math.round(accumulatedChange);
       let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
       if (shouldChangeByStepSize) {
@@ -51,7 +52,7 @@ const Angle = ({ element }: AngleProps) => {
     <DragInput
       label="A"
       value={Math.round(radianToDegree(element.angle) * 100) / 100}
-      element={element}
+      elements={[element]}
       dragInputCallback={handleDegreeChange}
       editable={isPropertyEditable(element, "angle")}
     />

+ 14 - 13
packages/excalidraw/components/Stats/Dimension.tsx

@@ -14,7 +14,7 @@ const _shouldKeepAspectRatio = (element: ExcalidrawElement) => {
   return element.type === "image";
 };
 
-const newOrigin = (
+export const newOrigin = (
   x1: number,
   y1: number,
   w1: number,
@@ -59,10 +59,11 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
     shouldChangeByStepSize,
     nextValue,
   ) => {
-    if (stateAtStart) {
+    const _stateAtStart = stateAtStart[0];
+    if (_stateAtStart) {
       const keepAspectRatio =
         shouldKeepAspectRatio || _shouldKeepAspectRatio(element);
-      const aspectRatio = stateAtStart.width / stateAtStart.height;
+      const aspectRatio = _stateAtStart.width / _stateAtStart.height;
 
       if (nextValue !== undefined) {
         const nextWidth = Math.max(
@@ -70,7 +71,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
             ? nextValue
             : keepAspectRatio
             ? nextValue * aspectRatio
-            : stateAtStart.width,
+            : _stateAtStart.width,
           0,
         );
         const nextHeight = Math.max(
@@ -78,7 +79,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
             ? nextValue
             : keepAspectRatio
             ? nextValue / aspectRatio
-            : stateAtStart.height,
+            : _stateAtStart.height,
           0,
         );
 
@@ -100,7 +101,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
       const changeInWidth = property === "width" ? accumulatedChange : 0;
       const changeInHeight = property === "height" ? accumulatedChange : 0;
 
-      let nextWidth = Math.max(0, stateAtStart.width + changeInWidth);
+      let nextWidth = Math.max(0, _stateAtStart.width + changeInWidth);
       if (property === "width") {
         if (shouldChangeByStepSize) {
           nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
@@ -109,7 +110,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
         }
       }
 
-      let nextHeight = Math.max(0, stateAtStart.height + changeInHeight);
+      let nextHeight = Math.max(0, _stateAtStart.height + changeInHeight);
       if (property === "height") {
         if (shouldChangeByStepSize) {
           nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
@@ -130,13 +131,13 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
         width: nextWidth,
         height: nextHeight,
         ...newOrigin(
-          stateAtStart.x,
-          stateAtStart.y,
-          stateAtStart.width,
-          stateAtStart.height,
+          _stateAtStart.x,
+          _stateAtStart.y,
+          _stateAtStart.width,
+          _stateAtStart.height,
           nextWidth,
           nextHeight,
-          stateAtStart.angle,
+          _stateAtStart.angle,
         ),
       });
     }
@@ -145,7 +146,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
   return (
     <DragInput
       label={property === "width" ? "W" : "H"}
-      element={element}
+      elements={[element]}
       dragInputCallback={handleDimensionChange}
       value={
         Math.round(

+ 9 - 6
packages/excalidraw/components/Stats/DragInput.tsx

@@ -11,7 +11,7 @@ import clsx from "clsx";
 export type DragInputCallbackType = (
   accumulatedChange: number,
   instantChange: number,
-  stateAtStart: ExcalidrawElement,
+  stateAtStart: ExcalidrawElement[],
   shouldKeepAspectRatio: boolean,
   shouldChangeByStepSize: boolean,
   nextValue?: number,
@@ -20,7 +20,7 @@ export type DragInputCallbackType = (
 interface StatsDragInputProps {
   label: string | React.ReactNode;
   value: number;
-  element: ExcalidrawElement;
+  elements: ExcalidrawElement[];
   editable?: boolean;
   shouldKeepAspectRatio?: boolean;
   dragInputCallback: DragInputCallbackType;
@@ -30,7 +30,7 @@ const StatsDragInput = ({
   label,
   dragInputCallback,
   value,
-  element,
+  elements,
   editable = true,
   shouldKeepAspectRatio,
 }: StatsDragInputProps) => {
@@ -64,7 +64,7 @@ const StatsDragInput = ({
               y: number;
             } | null = null;
 
-            let stateAtStart: ExcalidrawElement | null = null;
+            let stateAtStart: ExcalidrawElement[] | null = null;
 
             let accumulatedChange: number | null = null;
 
@@ -72,7 +72,9 @@ const StatsDragInput = ({
 
             const onPointerMove = (event: PointerEvent) => {
               if (!stateAtStart) {
-                stateAtStart = deepCopyElement(element);
+                stateAtStart = elements.map((element) =>
+                  deepCopyElement(element),
+                );
               }
 
               if (!accumulatedChange) {
@@ -146,11 +148,12 @@ const StatsDragInput = ({
               dragInputCallback(
                 0,
                 0,
-                element,
+                elements,
                 shouldKeepAspectRatio!!,
                 false,
                 v,
               );
+              eventTarget.blur();
             }
           }
         }}

+ 35 - 32
packages/excalidraw/components/Stats/FontSize.tsx

@@ -22,40 +22,43 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => {
     shouldChangeByStepSize,
     nextValue,
   ) => {
-    if (nextValue) {
-      const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
+    const _stateAtStart = stateAtStart[0];
+    if (_stateAtStart) {
+      if (nextValue) {
+        const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
 
-      const newElement = {
-        ...element,
-        fontSize: nextFontSize,
-      };
-      const updates = refreshTextDimensions(newElement, null, elementsMap);
-      mutateElement(element, {
-        ...updates,
-        fontSize: nextFontSize,
-      });
-      return;
-    }
+        const newElement = {
+          ...element,
+          fontSize: nextFontSize,
+        };
+        const updates = refreshTextDimensions(newElement, null, elementsMap);
+        mutateElement(element, {
+          ...updates,
+          fontSize: nextFontSize,
+        });
+        return;
+      }
 
-    if (stateAtStart && stateAtStart.type === "text") {
-      const originalFontSize = Math.round(stateAtStart.fontSize);
-      const changeInFontSize = Math.round(accumulatedChange);
-      let nextFontSize = Math.max(
-        originalFontSize + changeInFontSize,
-        MIN_FONT_SIZE,
-      );
-      if (shouldChangeByStepSize) {
-        nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
+      if (_stateAtStart.type === "text") {
+        const originalFontSize = Math.round(_stateAtStart.fontSize);
+        const changeInFontSize = Math.round(accumulatedChange);
+        let nextFontSize = Math.max(
+          originalFontSize + changeInFontSize,
+          MIN_FONT_SIZE,
+        );
+        if (shouldChangeByStepSize) {
+          nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
+        }
+        const newElement = {
+          ...element,
+          fontSize: nextFontSize,
+        };
+        const updates = refreshTextDimensions(newElement, null, elementsMap);
+        mutateElement(element, {
+          ...updates,
+          fontSize: nextFontSize,
+        });
       }
-      const newElement = {
-        ...element,
-        fontSize: nextFontSize,
-      };
-      const updates = refreshTextDimensions(newElement, null, elementsMap);
-      mutateElement(element, {
-        ...updates,
-        fontSize: nextFontSize,
-      });
     }
   };
 
@@ -63,7 +66,7 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => {
     <StatsDragInput
       label="F"
       value={Math.round(element.fontSize * 10) / 10}
-      element={element}
+      elements={[element]}
       dragInputCallback={handleFontSizeChange}
     />
   );

+ 143 - 0
packages/excalidraw/components/Stats/MultiDimension.tsx

@@ -0,0 +1,143 @@
+import { getCommonBounds } from "../../element";
+import { mutateElement } from "../../element/mutateElement";
+import type { ExcalidrawElement } from "../../element/types";
+import DragInput from "./DragInput";
+import type { DragInputCallbackType } from "./DragInput";
+import { getStepSizedValue } from "./utils";
+
+interface MultiDimensionProps {
+  property: "width" | "height";
+  elements: ExcalidrawElement[];
+}
+
+const STEP_SIZE = 10;
+
+const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
+  const handleDimensionChange: DragInputCallbackType = (
+    accumulatedChange,
+    instantChange,
+    stateAtStart,
+    shouldKeepAspectRatio,
+    shouldChangeByStepSize,
+    nextValue,
+  ) => {
+    const [x1, y1, x2, y2] = getCommonBounds(stateAtStart);
+    const initialWidth = x2 - x1;
+    const initialHeight = y2 - y1;
+    const keepAspectRatio = true;
+    const aspectRatio = initialWidth / initialHeight;
+
+    if (nextValue !== undefined) {
+      const nextHeight =
+        property === "height" ? nextValue : nextValue / aspectRatio;
+
+      const scale = nextHeight / initialHeight;
+      const anchorX = property === "width" ? x1 : x1 + width / 2;
+      const anchorY = property === "height" ? y1 : y1 + height / 2;
+
+      let i = 0;
+      while (i < stateAtStart.length) {
+        const element = elements[i];
+        const origElement = stateAtStart[i];
+
+        // it should never happen that element and origElement are different
+        // but check just in case
+        if (element.id === origElement.id) {
+          const offsetX = origElement.x - anchorX;
+          const offsetY = origElement.y - anchorY;
+          const nextWidth = origElement.width * scale;
+          const nextHeight = origElement.height * scale;
+          const x = anchorX + offsetX * scale;
+          const y = anchorY + offsetY * scale;
+
+          mutateElement(
+            element,
+            {
+              width: nextWidth,
+              height: nextHeight,
+              x,
+              y,
+            },
+            i === stateAtStart.length - 1,
+          );
+        }
+        i++;
+      }
+
+      return;
+    }
+
+    const changeInWidth = property === "width" ? accumulatedChange : 0;
+    const changeInHeight = property === "height" ? accumulatedChange : 0;
+
+    let nextWidth = Math.max(0, initialWidth + changeInWidth);
+    if (property === "width") {
+      if (shouldChangeByStepSize) {
+        nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
+      } else {
+        nextWidth = Math.round(nextWidth);
+      }
+    }
+
+    let nextHeight = Math.max(0, initialHeight + changeInHeight);
+    if (property === "height") {
+      if (shouldChangeByStepSize) {
+        nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
+      } else {
+        nextHeight = Math.round(nextHeight);
+      }
+    }
+
+    if (keepAspectRatio) {
+      if (property === "width") {
+        nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100;
+      } else {
+        nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100;
+      }
+    }
+
+    const scale = nextHeight / initialHeight;
+    const anchorX = property === "width" ? x1 : x1 + width / 2;
+    const anchorY = property === "height" ? y1 : y1 + height / 2;
+
+    let i = 0;
+    while (i < stateAtStart.length) {
+      const element = elements[i];
+      const origElement = stateAtStart[i];
+
+      const offsetX = origElement.x - anchorX;
+      const offsetY = origElement.y - anchorY;
+      const nextWidth = origElement.width * scale;
+      const nextHeight = origElement.height * scale;
+      const x = anchorX + offsetX * scale;
+      const y = anchorY + offsetY * scale;
+
+      mutateElement(
+        element,
+        {
+          width: nextWidth,
+          height: nextHeight,
+          x,
+          y,
+        },
+        i === stateAtStart.length - 1,
+      );
+      i++;
+    }
+  };
+
+  const [x1, y1, x2, y2] = getCommonBounds(elements);
+  const width = x2 - x1;
+  const height = y2 - y1;
+
+  return (
+    <DragInput
+      label={property === "width" ? "W" : "H"}
+      elements={elements}
+      dragInputCallback={handleDimensionChange}
+      value={Math.round((property === "width" ? width : height) * 100) / 100}
+    />
+  );
+};
+
+export default MultiDimension;

+ 8 - 0
packages/excalidraw/components/Stats/index.scss

@@ -23,6 +23,14 @@
       margin-bottom: 8px;
     }
 
+    .elementsCount {
+      width: 100%;
+      font-size: 12px;
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 12px;
+    }
+
     .statsItem {
       width: 100%;
       margin-bottom: 4px;

+ 47 - 13
packages/excalidraw/components/Stats/index.tsx

@@ -13,6 +13,8 @@ import Angle from "./Angle";
 
 import "./index.scss";
 import FontSize from "./FontSize";
+import MultiDimension from "./MultiDimension";
+import { elementsAreInSameGroup } from "../../groups";
 
 interface StatsProps {
   appState: AppState;
@@ -32,6 +34,9 @@ export const Stats = (props: StatsProps) => {
   const singleElement =
     selectedElements.length === 1 ? selectedElements[0] : null;
 
+  const multipleElements =
+    selectedElements.length > 1 ? selectedElements : null;
+
   const [sceneDimension, setSceneDimension] = useState<{
     width: number;
     height: number;
@@ -91,7 +96,7 @@ export const Stats = (props: StatsProps) => {
           </table>
         </div>
 
-        {singleElement && (
+        {selectedElements.length > 0 && (
           <div
             className="section"
             style={{
@@ -100,22 +105,51 @@ export const Stats = (props: StatsProps) => {
           >
             <h3>{t("stats.elementStats")}</h3>
 
-            <div className="sectionContent">
-              <div className="elementType">
-                {t(`element.${singleElement.type}`)}
+            {singleElement && (
+              <div className="sectionContent">
+                <div className="elementType">
+                  {t(`element.${singleElement.type}`)}
+                </div>
+
+                <div className="statsItem">
+                  <Dimension property="width" element={singleElement} />
+                  <Dimension property="height" element={singleElement} />
+                  <Angle element={singleElement} />
+                  {singleElement.type === "text" && (
+                    <FontSize
+                      element={singleElement}
+                      elementsMap={elementsMap}
+                    />
+                  )}
+                </div>
+
+                {singleElement.type === "text" && <div></div>}
               </div>
+            )}
 
-              <div className="statsItem">
-                <Dimension property="width" element={singleElement} />
-                <Dimension property="height" element={singleElement} />
-                <Angle element={singleElement} />
-                {singleElement.type === "text" && (
-                  <FontSize element={singleElement} elementsMap={elementsMap} />
+            {multipleElements && (
+              <div className="sectionContent">
+                {elementsAreInSameGroup(multipleElements) && (
+                  <div className="elementType">{t("element.group")}</div>
                 )}
-              </div>
 
-              {singleElement.type === "text" && <div></div>}
-            </div>
+                <div className="elementsCount">
+                  <div>{t("stats.elements")}</div>
+                  <div>{selectedElements.length}</div>
+                </div>
+
+                <div className="statsItem">
+                  <MultiDimension
+                    property="width"
+                    elements={multipleElements}
+                  />
+                  <MultiDimension
+                    property="height"
+                    elements={multipleElements}
+                  />
+                </div>
+              </div>
+            )}
           </div>
         )}
       </Island>