فهرست منبع

handle bound texts

Ryan Di 1 سال پیش
والد
کامیت
c68c2be44c

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

@@ -1,5 +1,7 @@
 import { mutateElement } from "../../element/mutateElement";
-import type { ExcalidrawElement } from "../../element/types";
+import { getBoundTextElement } from "../../element/textElement";
+import { isArrowElement } from "../../element/typeChecks";
+import type { ElementsMap, ExcalidrawElement } from "../../element/types";
 import { degreeToRadian, radianToDegree } from "../../math";
 import DragInput from "./DragInput";
 import type { DragInputCallbackType } from "./DragInput";
@@ -7,19 +9,18 @@ import { getStepSizedValue, isPropertyEditable } from "./utils";
 
 interface AngleProps {
   element: ExcalidrawElement;
+  elementsMap: ElementsMap;
 }
 
 const STEP_SIZE = 15;
 
-const Angle = ({ element }: AngleProps) => {
-  const handleDegreeChange: DragInputCallbackType = (
+const Angle = ({ element, elementsMap }: AngleProps) => {
+  const handleDegreeChange: DragInputCallbackType = ({
     accumulatedChange,
-    instantChange,
     stateAtStart,
-    shouldKeepAspectRatio,
     shouldChangeByStepSize,
     nextValue,
-  ) => {
+  }) => {
     const _stateAtStart = stateAtStart[0];
     if (_stateAtStart) {
       if (nextValue !== undefined) {
@@ -27,6 +28,12 @@ const Angle = ({ element }: AngleProps) => {
         mutateElement(element, {
           angle: nextAngle,
         });
+
+        const boundTextElement = getBoundTextElement(element, elementsMap);
+        if (boundTextElement && !isArrowElement(element)) {
+          mutateElement(boundTextElement, { angle: nextAngle });
+        }
+
         return;
       }
 
@@ -38,13 +45,19 @@ const Angle = ({ element }: AngleProps) => {
         nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE);
       }
 
+      nextAngleInDegrees =
+        nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
+
+      const nextAngle = degreeToRadian(nextAngleInDegrees);
+
       mutateElement(element, {
-        angle: degreeToRadian(
-          nextAngleInDegrees < 0
-            ? nextAngleInDegrees + 360
-            : nextAngleInDegrees,
-        ),
+        angle: nextAngle,
       });
+
+      const boundTextElement = getBoundTextElement(element, elementsMap);
+      if (boundTextElement && !isArrowElement(element)) {
+        mutateElement(boundTextElement, { angle: nextAngle });
+      }
     }
   };
 

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

@@ -1,13 +1,26 @@
-import type { ExcalidrawElement } from "../../element/types";
+import type { ElementsMap, ExcalidrawElement } from "../../element/types";
 import DragInput from "./DragInput";
 import type { DragInputCallbackType } from "./DragInput";
 import { getStepSizedValue, isPropertyEditable } from "./utils";
 import { mutateElement } from "../../element/mutateElement";
-import { rescalePointsInElement } from "../../element/resizeElements";
+import {
+  measureFontSizeFromWidth,
+  rescalePointsInElement,
+} from "../../element/resizeElements";
+import {
+  getApproxMinLineHeight,
+  getApproxMinLineWidth,
+  getBoundTextElement,
+  getBoundTextMaxWidth,
+  handleBindTextResize,
+} from "../../element/textElement";
+import { getFontString } from "../../utils";
+import { updateBoundElements } from "../../element/binding";
 
 interface DimensionDragInputProps {
   property: "width" | "height";
   element: ExcalidrawElement;
+  elementsMap: ElementsMap;
 }
 
 const STEP_SIZE = 10;
@@ -51,13 +64,16 @@ export const newOrigin = (
   };
 };
 
-const getResizedUpdates = (
+const resizeElement = (
   nextWidth: number,
   nextHeight: number,
+  keepAspectRatio: boolean,
   latestState: ExcalidrawElement,
   stateAtStart: ExcalidrawElement,
+  elementsMap: ElementsMap,
+  originalElementsMap: Map<string, ExcalidrawElement>,
 ) => {
-  return {
+  mutateElement(latestState, {
     ...newOrigin(
       latestState.x,
       latestState.y,
@@ -70,18 +86,72 @@ const getResizedUpdates = (
     width: nextWidth,
     height: nextHeight,
     ...rescalePointsInElement(stateAtStart, nextWidth, nextHeight, true),
-  };
+  });
+
+  let boundTextFont: { fontSize?: number } = {};
+  const boundTextElement = getBoundTextElement(latestState, elementsMap);
+
+  if (boundTextElement) {
+    boundTextFont = {
+      fontSize: boundTextElement.fontSize,
+    };
+    if (keepAspectRatio) {
+      const updatedElement = {
+        ...latestState,
+        width: nextWidth,
+        height: nextHeight,
+      };
+
+      const nextFont = measureFontSizeFromWidth(
+        boundTextElement,
+        elementsMap,
+        getBoundTextMaxWidth(updatedElement, boundTextElement),
+      );
+      boundTextFont = {
+        fontSize: nextFont?.size ?? boundTextElement.fontSize,
+      };
+    } else {
+      const minWidth = getApproxMinLineWidth(
+        getFontString(boundTextElement),
+        boundTextElement.lineHeight,
+      );
+      const minHeight = getApproxMinLineHeight(
+        boundTextElement.fontSize,
+        boundTextElement.lineHeight,
+      );
+      nextWidth = Math.max(nextWidth, minWidth);
+      nextHeight = Math.max(nextHeight, minHeight);
+    }
+  }
+
+  updateBoundElements(latestState, elementsMap, {
+    newSize: {
+      width: nextWidth,
+      height: nextHeight,
+    },
+  });
+
+  if (boundTextElement && boundTextFont) {
+    mutateElement(boundTextElement, {
+      fontSize: boundTextFont.fontSize,
+    });
+  }
+  handleBindTextResize(latestState, elementsMap, "e", keepAspectRatio);
 };
 
-const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
-  const handleDimensionChange: DragInputCallbackType = (
+const DimensionDragInput = ({
+  property,
+  element,
+  elementsMap,
+}: DimensionDragInputProps) => {
+  const handleDimensionChange: DragInputCallbackType = ({
     accumulatedChange,
-    instantChange,
     stateAtStart,
+    originalElementsMap,
     shouldKeepAspectRatio,
     shouldChangeByStepSize,
     nextValue,
-  ) => {
+  }) => {
     const _stateAtStart = stateAtStart[0];
     if (_stateAtStart) {
       const keepAspectRatio =
@@ -106,10 +176,16 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
           0,
         );
 
-        mutateElement(
+        resizeElement(
+          nextWidth,
+          nextHeight,
+          keepAspectRatio,
           element,
-          getResizedUpdates(nextWidth, nextHeight, element, _stateAtStart),
+          _stateAtStart,
+          elementsMap,
+          originalElementsMap,
         );
+
         return;
       }
       const changeInWidth = property === "width" ? accumulatedChange : 0;
@@ -141,9 +217,14 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
         }
       }
 
-      mutateElement(
+      resizeElement(
+        nextWidth,
+        nextHeight,
+        keepAspectRatio,
         element,
-        getResizedUpdates(nextWidth, nextHeight, element, _stateAtStart),
+        _stateAtStart,
+        elementsMap,
+        originalElementsMap,
       );
     }
   };

+ 45 - 21
packages/excalidraw/components/Stats/DragInput.tsx

@@ -2,21 +2,30 @@ import { useEffect, useMemo, useRef, useState } from "react";
 import throttle from "lodash.throttle";
 import { EVENT } from "../../constants";
 import { KEYS } from "../../keys";
-import type { ExcalidrawElement } from "../../element/types";
+import type { ElementsMap, ExcalidrawElement } from "../../element/types";
 import { deepCopyElement } from "../../element/newElement";
 
 import "./DragInput.scss";
 import clsx from "clsx";
 import { useApp } from "../App";
 
-export type DragInputCallbackType = (
-  accumulatedChange: number,
-  instantChange: number,
-  stateAtStart: ExcalidrawElement[],
-  shouldKeepAspectRatio: boolean,
-  shouldChangeByStepSize: boolean,
-  nextValue?: number,
-) => void;
+export type DragInputCallbackType = ({
+  accumulatedChange,
+  instantChange,
+  stateAtStart,
+  originalElementsMap,
+  shouldKeepAspectRatio,
+  shouldChangeByStepSize,
+  nextValue,
+}: {
+  accumulatedChange: number;
+  instantChange: number;
+  stateAtStart: ExcalidrawElement[];
+  originalElementsMap: ElementsMap;
+  shouldKeepAspectRatio: boolean;
+  shouldChangeByStepSize: boolean;
+  nextValue?: number;
+}) => void;
 
 interface StatsDragInputProps {
   label: string | React.ReactNode;
@@ -67,6 +76,8 @@ const StatsDragInput = ({
             } | null = null;
 
             let stateAtStart: ExcalidrawElement[] | null = null;
+            let originalElementsMap: Map<string, ExcalidrawElement> | null =
+              null;
 
             let accumulatedChange: number | null = null;
 
@@ -79,6 +90,15 @@ const StatsDragInput = ({
                 );
               }
 
+              if (!originalElementsMap) {
+                originalElementsMap = app.scene
+                  .getNonDeletedElements()
+                  .reduce((acc, element) => {
+                    acc.set(element.id, deepCopyElement(element));
+                    return acc;
+                  }, new Map() as ElementsMap);
+              }
+
               if (!accumulatedChange) {
                 accumulatedChange = 0;
               }
@@ -87,13 +107,14 @@ const StatsDragInput = ({
                 const instantChange = event.clientX - lastPointer.x;
                 accumulatedChange += instantChange;
 
-                cbThrottled(
+                cbThrottled({
                   accumulatedChange,
                   instantChange,
                   stateAtStart,
-                  shouldKeepAspectRatio!!,
-                  event.shiftKey,
-                );
+                  originalElementsMap,
+                  shouldKeepAspectRatio: shouldKeepAspectRatio!!,
+                  shouldChangeByStepSize: event.shiftKey,
+                });
               }
 
               lastPointer = {
@@ -117,6 +138,7 @@ const StatsDragInput = ({
                 lastPointer = null;
                 accumulatedChange = null;
                 stateAtStart = null;
+                originalElementsMap = null;
 
                 document.body.classList.remove("dragResize");
               },
@@ -149,14 +171,16 @@ const StatsDragInput = ({
                 setInputValue(value.toString());
                 return;
               }
-              dragInputCallback(
-                0,
-                0,
-                elements,
-                shouldKeepAspectRatio!!,
-                false,
-                v,
-              );
+
+              dragInputCallback({
+                accumulatedChange: 0,
+                instantChange: 0,
+                stateAtStart: elements,
+                originalElementsMap: app.scene.getNonDeletedElementsMap(),
+                shouldKeepAspectRatio: shouldKeepAspectRatio!!,
+                shouldChangeByStepSize: false,
+                nextValue: v,
+              });
               app.store.shouldCaptureIncrement();
               eventTarget.blur();
             }

+ 2 - 4
packages/excalidraw/components/Stats/FontSize.tsx

@@ -14,14 +14,12 @@ const MIN_FONT_SIZE = 4;
 const STEP_SIZE = 4;
 
 const FontSize = ({ element, elementsMap }: FontSizeProps) => {
-  const handleFontSizeChange: DragInputCallbackType = (
+  const handleFontSizeChange: DragInputCallbackType = ({
     accumulatedChange,
-    instantChange,
     stateAtStart,
-    shouldKeepAspectRatio,
     shouldChangeByStepSize,
     nextValue,
-  ) => {
+  }) => {
     const _stateAtStart = stateAtStart[0];
     if (_stateAtStart) {
       if (nextValue) {

+ 83 - 17
packages/excalidraw/components/Stats/MultiDimension.tsx

@@ -1,7 +1,12 @@
-import { getCommonBounds } from "../../element";
+import { getCommonBounds, isTextElement } from "../../element";
+import { updateBoundElements } from "../../element/binding";
 import { mutateElement } from "../../element/mutateElement";
 import { rescalePointsInElement } from "../../element/resizeElements";
-import type { ExcalidrawElement } from "../../element/types";
+import {
+  getBoundTextElement,
+  handleBindTextResize,
+} from "../../element/textElement";
+import type { ElementsMap, ExcalidrawElement } from "../../element/types";
 import DragInput from "./DragInput";
 import type { DragInputCallbackType } from "./DragInput";
 import { getStepSizedValue } from "./utils";
@@ -9,6 +14,7 @@ import { getStepSizedValue } from "./utils";
 interface MultiDimensionProps {
   property: "width" | "height";
   elements: ExcalidrawElement[];
+  elementsMap: ElementsMap;
 }
 
 const STEP_SIZE = 10;
@@ -32,18 +38,66 @@ const getResizedUpdates = (
     x,
     y,
     ...rescalePointsInElement(stateAtStart, nextWidth, nextHeight, false),
+    ...(isTextElement(stateAtStart)
+      ? { fontSize: stateAtStart.fontSize * scale }
+      : {}),
   };
 };
 
-const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
-  const handleDimensionChange: DragInputCallbackType = (
+const resizeElement = (
+  anchorX: number,
+  anchorY: number,
+  property: MultiDimensionProps["property"],
+  scale: number,
+  latestElement: ExcalidrawElement,
+  origElement: ExcalidrawElement,
+  elementsMap: ElementsMap,
+  originalElementsMap: ElementsMap,
+  shouldInformMutation: boolean,
+) => {
+  const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
+
+  mutateElement(latestElement, updates, shouldInformMutation);
+  const boundTextElement = getBoundTextElement(
+    origElement,
+    originalElementsMap,
+  );
+  if (boundTextElement) {
+    const newFontSize = boundTextElement.fontSize * scale;
+    updateBoundElements(latestElement, elementsMap, {
+      newSize: { width: updates.width, height: updates.height },
+    });
+    const latestBoundTextElement = elementsMap.get(boundTextElement.id);
+    if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
+      mutateElement(
+        latestBoundTextElement,
+        {
+          fontSize: newFontSize,
+        },
+        shouldInformMutation,
+      );
+      handleBindTextResize(
+        latestElement,
+        elementsMap,
+        property === "width" ? "e" : "s",
+        true,
+      );
+    }
+  }
+};
+
+const MultiDimension = ({
+  property,
+  elements,
+  elementsMap,
+}: MultiDimensionProps) => {
+  const handleDimensionChange: DragInputCallbackType = ({
     accumulatedChange,
-    instantChange,
     stateAtStart,
-    shouldKeepAspectRatio,
+    originalElementsMap,
     shouldChangeByStepSize,
     nextValue,
-  ) => {
+  }) => {
     const [x1, y1, x2, y2] = getCommonBounds(stateAtStart);
     const initialWidth = x2 - x1;
     const initialHeight = y2 - y1;
@@ -60,15 +114,21 @@ const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
 
       let i = 0;
       while (i < stateAtStart.length) {
-        const element = elements[i];
+        const latestElement = 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) {
-          mutateElement(
-            element,
-            getResizedUpdates(anchorX, anchorY, scale, origElement),
+        if (latestElement.id === origElement.id) {
+          resizeElement(
+            anchorX,
+            anchorY,
+            property,
+            scale,
+            latestElement,
+            origElement,
+            elementsMap,
+            originalElementsMap,
             i === stateAtStart.length - 1,
           );
         }
@@ -113,13 +173,19 @@ const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
 
     let i = 0;
     while (i < stateAtStart.length) {
-      const element = elements[i];
+      const latestElement = elements[i];
       const origElement = stateAtStart[i];
 
-      if (element.id === origElement.id) {
-        mutateElement(
-          element,
-          getResizedUpdates(anchorX, anchorY, scale, origElement),
+      if (latestElement.id === origElement.id) {
+        resizeElement(
+          anchorX,
+          anchorY,
+          property,
+          scale,
+          latestElement,
+          origElement,
+          elementsMap,
+          originalElementsMap,
           i === stateAtStart.length - 1,
         );
       }

+ 22 - 5
packages/excalidraw/components/Stats/index.tsx

@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
 import { getCommonBounds } from "../../element/bounds";
 import type { NonDeletedExcalidrawElement } from "../../element/types";
 import { t } from "../../i18n";
-import { getTargetElements } from "../../scene";
+import { getSelectedElements } from "../../scene";
 import type Scene from "../../scene/Scene";
 import type { AppState, ExcalidrawProps } from "../../types";
 import { CloseIcon } from "../icons";
@@ -29,7 +29,14 @@ export const Stats = (props: StatsProps) => {
   const elements = props.scene.getNonDeletedElements();
   const elementsMap = props.scene.getNonDeletedElementsMap();
   const sceneNonce = props.scene.getSceneNonce();
-  const selectedElements = getTargetElements(elements, props.appState);
+  // const selectedElements = getTargetElements(elements, props.appState);
+  const selectedElements = getSelectedElements(
+    props.scene.getNonDeletedElementsMap(),
+    props.appState,
+    {
+      includeBoundTextElement: false,
+    },
+  );
 
   const singleElement =
     selectedElements.length === 1 ? selectedElements[0] : null;
@@ -112,9 +119,17 @@ export const Stats = (props: StatsProps) => {
                 </div>
 
                 <div className="statsItem">
-                  <Dimension property="width" element={singleElement} />
-                  <Dimension property="height" element={singleElement} />
-                  <Angle element={singleElement} />
+                  <Dimension
+                    property="width"
+                    element={singleElement}
+                    elementsMap={elementsMap}
+                  />
+                  <Dimension
+                    property="height"
+                    element={singleElement}
+                    elementsMap={elementsMap}
+                  />
+                  <Angle element={singleElement} elementsMap={elementsMap} />
                   {singleElement.type === "text" && (
                     <FontSize
                       element={singleElement}
@@ -142,10 +157,12 @@ export const Stats = (props: StatsProps) => {
                   <MultiDimension
                     property="width"
                     elements={multipleElements}
+                    elementsMap={elementsMap}
                   />
                   <MultiDimension
                     property="height"
                     elements={multipleElements}
+                    elementsMap={elementsMap}
                   />
                 </div>
               </div>

+ 1 - 1
packages/excalidraw/element/resizeElements.ts

@@ -199,7 +199,7 @@ export const rescalePointsInElement = (
       }
     : {};
 
-const measureFontSizeFromWidth = (
+export const measureFontSizeFromWidth = (
   element: NonDeleted<ExcalidrawTextElement>,
   elementsMap: ElementsMap,
   nextWidth: number,