浏览代码

refactor to include dimension and step size

Ryan Di 1 年之前
父节点
当前提交
0987c5b770

+ 0 - 270
packages/excalidraw/components/DragInput.tsx

@@ -1,270 +0,0 @@
-import throttle from "lodash.throttle";
-import { useEffect, useMemo, useRef } from "react";
-import { EVENT } from "../constants";
-import { getTransformHandles } from "../element";
-import { mutateElement } from "../element/mutateElement";
-import { resizeSingleElement } from "../element/resizeElements";
-import type { ElementsMap, ExcalidrawElement } from "../element/types";
-import { KEYS } from "../keys";
-import { degreeToRadian, radianToDegree } from "../math";
-import type { AppState, Point } from "../types";
-import { deepCopyElement } from "../element/newElement";
-
-const shouldKeepAspectRatio = (element: ExcalidrawElement) => {
-  return element.type === "image";
-};
-
-type AdjustableProperty = "width" | "height" | "angle" | "x" | "y";
-
-interface DragInputProps {
-  label: string | React.ReactNode;
-  property: AdjustableProperty;
-  element: ExcalidrawElement;
-  elementsMap: ElementsMap;
-  zoom: AppState["zoom"];
-}
-
-const DragInput = ({
-  label,
-  property,
-  element,
-  elementsMap,
-  zoom,
-}: DragInputProps) => {
-  const inputRef = useRef<HTMLInputElement>(null);
-  const labelRef = useRef<HTMLDivElement>(null);
-
-  const originalElement = useRef<ExcalidrawElement>();
-  const accumulatedDimensionChange = useRef(0);
-
-  const handleChange = useMemo(
-    () =>
-      (
-        initialValue: number,
-        delta: number,
-        source: "pointerMove" | "keyDown",
-        pointerOffset?: number,
-      ) => {
-        if (inputRef.current && originalElement.current) {
-          const keepAspectRatio = shouldKeepAspectRatio(element);
-
-          if (
-            (property === "width" || property === "height") &&
-            source === "pointerMove" &&
-            pointerOffset
-          ) {
-            const handles = getTransformHandles(
-              originalElement.current,
-              zoom,
-              elementsMap,
-              "mouse",
-              {},
-            );
-
-            let referencePoint: Point | undefined;
-            let handleDirection: "e" | "s" | "se" | undefined;
-
-            if (keepAspectRatio && handles.se) {
-              referencePoint = [handles.se[0], handles.se[1]];
-              handleDirection = "se";
-            } else if (property === "width" && handles.e) {
-              referencePoint = [handles.e[0], handles.e[1]];
-              handleDirection = "e";
-            } else if (property === "height" && handles.s) {
-              referencePoint = [handles.s[0], handles.s[1]];
-              handleDirection = "s";
-            }
-
-            if (referencePoint !== undefined && handleDirection !== undefined) {
-              accumulatedDimensionChange.current += pointerOffset;
-
-              const pointer: Point = [
-                referencePoint[0] +
-                  (property === "width"
-                    ? accumulatedDimensionChange.current
-                    : 0),
-                referencePoint[1] +
-                  (property === "height"
-                    ? accumulatedDimensionChange.current
-                    : 0),
-              ];
-
-              resizeSingleElement(
-                elementsMap,
-                keepAspectRatio,
-                element,
-                elementsMap,
-                handleDirection,
-                false,
-                pointer[0],
-                pointer[1],
-              );
-            }
-          } else if (
-            source === "keyDown" ||
-            (source === "pointerMove" &&
-              property !== "width" &&
-              property !== "height")
-          ) {
-            const incVal = Math.round(
-              Math.sign(delta) * Math.pow(Math.abs(delta) / 10, 1.6),
-            );
-            let newVal = initialValue + incVal;
-
-            newVal =
-              property === "angle"
-                ? // so the degree converted from radian is an integer
-                  degreeToRadian(
-                    Math.round(
-                      radianToDegree(
-                        degreeToRadian(
-                          Math.sign(newVal % 360) === -1
-                            ? (newVal % 360) + 360
-                            : newVal % 360,
-                        ),
-                      ),
-                    ),
-                  )
-                : Math.round(newVal);
-
-            mutateElement(element, {
-              [property]: newVal,
-            });
-            originalElement.current = deepCopyElement(element);
-          }
-        }
-      },
-    [element, property, zoom, elementsMap],
-  );
-
-  const hangleChangeThrottled = useMemo(() => {
-    return throttle(handleChange, 16);
-  }, [handleChange]);
-
-  useEffect(() => {
-    const value =
-      Math.round(
-        property === "angle"
-          ? radianToDegree(element[property]) * 100
-          : element[property] * 100,
-      ) / 100;
-
-    if (inputRef.current) {
-      inputRef.current.value = String(value);
-    }
-  }, [element, element.version, element.versionNonce, property]);
-
-  useEffect(() => {
-    hangleChangeThrottled.cancel();
-  });
-
-  useEffect(() => {
-    accumulatedDimensionChange.current = 0;
-    originalElement.current = undefined;
-  }, [element.id]);
-
-  return (
-    <label className="color-input-container">
-      <div
-        className="color-picker-hash"
-        ref={labelRef}
-        style={{
-          width: "20px",
-        }}
-        onPointerDown={(event) => {
-          if (!originalElement.current) {
-            originalElement.current = deepCopyElement(element);
-          }
-
-          if (!accumulatedDimensionChange.current) {
-            accumulatedDimensionChange.current = 0;
-          }
-
-          if (inputRef.current) {
-            const startPosition = event.clientX;
-            let startValue = Number(inputRef.current.value);
-            if (isNaN(startValue)) {
-              startValue = 0;
-            }
-
-            let lastPointerRef: {
-              x: number;
-              y: number;
-            } | null = null;
-
-            document.body.classList.add("dragResize");
-
-            const onPointerMove = (event: PointerEvent) => {
-              if (lastPointerRef) {
-                hangleChangeThrottled(
-                  startValue,
-                  Math.ceil(event.clientX - startPosition),
-                  "pointerMove",
-                  event.clientX - lastPointerRef.x,
-                );
-              }
-
-              lastPointerRef = {
-                x: event.clientX,
-                y: event.clientY,
-              };
-            };
-
-            window.addEventListener(EVENT.POINTER_MOVE, onPointerMove, false);
-            window.addEventListener(
-              EVENT.POINTER_UP,
-              () => {
-                window.removeEventListener(
-                  EVENT.POINTER_MOVE,
-                  onPointerMove,
-                  false,
-                );
-
-                lastPointerRef = null;
-
-                document.body.classList.remove("dragResize");
-              },
-              false,
-            );
-          }
-        }}
-        onPointerUp={(event) => {
-          accumulatedDimensionChange.current = 0;
-          originalElement.current = undefined;
-        }}
-        onPointerEnter={() => {
-          if (labelRef.current) {
-            labelRef.current.style.cursor = "ew-resize";
-          }
-        }}
-      >
-        {label}
-      </div>
-      <input
-        className="color-picker-input"
-        style={{
-          width: "66px",
-          fontSize: "12px",
-        }}
-        autoComplete="off"
-        spellCheck="false"
-        onKeyDown={(event) => {
-          const eventTarget = event.target;
-
-          if (eventTarget instanceof HTMLInputElement) {
-            const value = Number(eventTarget.value);
-            if (isNaN(value)) {
-              return;
-            }
-            if (event.key === KEYS.ENTER) {
-              handleChange(value, 0, "keyDown");
-            }
-          }
-        }}
-        ref={inputRef}
-      ></input>
-    </label>
-  );
-};
-
-export default DragInput;

+ 61 - 0
packages/excalidraw/components/Stats/Angle.tsx

@@ -0,0 +1,61 @@
+import { mutateElement } from "../../element/mutateElement";
+import type { ExcalidrawElement } from "../../element/types";
+import { degreeToRadian, radianToDegree } from "../../math";
+import DragInput from "./DragInput";
+import type { DragInputCallbackType } from "./DragInput";
+import { getStepSizedValue, isPropertyEditable } from "./utils";
+
+interface AngleProps {
+  element: ExcalidrawElement;
+}
+
+const STEP_SIZE = 15;
+
+const Angle = ({ element }: AngleProps) => {
+  const handleDegreeChange: DragInputCallbackType = (
+    accumulatedChange,
+    instantChange,
+    stateAtStart,
+    shouldKeepAspectRatio,
+    shouldChangeByStepSize,
+    nextValue,
+  ) => {
+    if (nextValue !== undefined) {
+      const nextAngle = degreeToRadian(nextValue);
+      mutateElement(element, {
+        angle: nextAngle,
+      });
+      return;
+    }
+
+    if (stateAtStart) {
+      const originalAngleInDegrees =
+        Math.round(radianToDegree(stateAtStart.angle) * 100) / 100;
+      const changeInDegrees = Math.round(accumulatedChange);
+      let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
+      if (shouldChangeByStepSize) {
+        nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE);
+      }
+
+      mutateElement(element, {
+        angle: degreeToRadian(
+          nextAngleInDegrees < 0
+            ? nextAngleInDegrees + 360
+            : nextAngleInDegrees,
+        ),
+      });
+    }
+  };
+
+  return (
+    <DragInput
+      label="A"
+      value={Math.round(radianToDegree(element.angle) * 100) / 100}
+      element={element}
+      dragInputCallback={handleDegreeChange}
+      editable={isPropertyEditable(element, "angle")}
+    />
+  );
+};
+
+export default Angle;

+ 160 - 0
packages/excalidraw/components/Stats/Dimension.tsx

@@ -0,0 +1,160 @@
+import type { ExcalidrawElement } from "../../element/types";
+import DragInput from "./DragInput";
+import type { DragInputCallbackType } from "./DragInput";
+import { getStepSizedValue, isPropertyEditable } from "./utils";
+import { mutateElement } from "../../element/mutateElement";
+
+interface DimensionDragInputProps {
+  property: "width" | "height";
+  element: ExcalidrawElement;
+}
+
+const STEP_SIZE = 10;
+const _shouldKeepAspectRatio = (element: ExcalidrawElement) => {
+  return element.type === "image";
+};
+
+const newOrigin = (
+  x1: number,
+  y1: number,
+  w1: number,
+  h1: number,
+  w2: number,
+  h2: number,
+  angle: number,
+) => {
+  /**
+   * The formula below is the result of solving
+   *   rotate(x1, y1, cx1, cy1, angle) = rotate(x2, y2, cx2, cy2, angle)
+   * where rotate is the function defined in math.ts
+   *
+   * This is so that the new origin (x2, y2),
+   * when rotated against the new center (cx2, cy2),
+   * coincides with (x1, y1) rotated against (cx1, cy1)
+   *
+   * The reason for doing this computation is so the element's top left corner
+   * on the canvas remains fixed after any changes in its dimension.
+   */
+
+  return {
+    x:
+      x1 +
+      (w1 - w2) / 2 +
+      ((w2 - w1) / 2) * Math.cos(angle) +
+      ((h1 - h2) / 2) * Math.sin(angle),
+    y:
+      y1 +
+      (h1 - h2) / 2 +
+      ((w2 - w1) / 2) * Math.sin(angle) +
+      ((h2 - h1) / 2) * Math.cos(angle),
+  };
+};
+
+const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
+  const handleDimensionChange: DragInputCallbackType = (
+    accumulatedChange,
+    instantChange,
+    stateAtStart,
+    shouldKeepAspectRatio,
+    shouldChangeByStepSize,
+    nextValue,
+  ) => {
+    if (stateAtStart) {
+      const keepAspectRatio =
+        shouldKeepAspectRatio || _shouldKeepAspectRatio(element);
+      const aspectRatio = stateAtStart.width / stateAtStart.height;
+
+      if (nextValue !== undefined) {
+        const nextWidth = Math.max(
+          property === "width"
+            ? nextValue
+            : keepAspectRatio
+            ? nextValue * aspectRatio
+            : stateAtStart.width,
+          0,
+        );
+        const nextHeight = Math.max(
+          property === "height"
+            ? nextValue
+            : keepAspectRatio
+            ? nextValue / aspectRatio
+            : stateAtStart.height,
+          0,
+        );
+
+        mutateElement(element, {
+          ...newOrigin(
+            element.x,
+            element.y,
+            element.width,
+            element.height,
+            nextWidth,
+            nextHeight,
+            element.angle,
+          ),
+          width: nextWidth,
+          height: nextHeight,
+        });
+        return;
+      }
+      const changeInWidth = property === "width" ? accumulatedChange : 0;
+      const changeInHeight = property === "height" ? accumulatedChange : 0;
+
+      let nextWidth = Math.max(0, stateAtStart.width + changeInWidth);
+      if (property === "width") {
+        if (shouldChangeByStepSize) {
+          nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
+        } else {
+          nextWidth = Math.round(nextWidth);
+        }
+      }
+
+      let nextHeight = Math.max(0, stateAtStart.height + 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;
+        }
+      }
+
+      mutateElement(element, {
+        width: nextWidth,
+        height: nextHeight,
+        ...newOrigin(
+          stateAtStart.x,
+          stateAtStart.y,
+          stateAtStart.width,
+          stateAtStart.height,
+          nextWidth,
+          nextHeight,
+          stateAtStart.angle,
+        ),
+      });
+    }
+  };
+
+  return (
+    <DragInput
+      label={property === "width" ? "W" : "H"}
+      element={element}
+      dragInputCallback={handleDimensionChange}
+      value={
+        Math.round(
+          (property === "width" ? element.width : element.height) * 100,
+        ) / 100
+      }
+      editable={isPropertyEditable(element, property)}
+    />
+  );
+};
+
+export default DimensionDragInput;

+ 75 - 0
packages/excalidraw/components/Stats/DragInput.scss

@@ -0,0 +1,75 @@
+.excalidraw {
+  .drag-input-container {
+    display: flex;
+    width: 100%;
+
+    &:focus-within {
+      box-shadow: 0 0 0 1px var(--color-primary-darkest);
+      border-radius: var(--border-radius-lg);
+    }
+  }
+
+  .disabled {
+    opacity: 0.5;
+    pointer-events: none;
+  }
+
+  .drag-input-label {
+    height: var(--default-button-size);
+    flex-shrink: 0;
+    padding: 0.5rem 0.5rem 0.5rem 0.75rem;
+    border: 1px solid var(--default-border-color);
+    border-right: 0;
+    box-sizing: border-box;
+
+    :root[dir="ltr"] & {
+      border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
+    }
+
+    :root[dir="rtl"] & {
+      border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
+      border-right: 1px solid var(--default-border-color);
+      border-left: 0;
+    }
+
+    color: var(--input-label-color);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+  }
+
+  .drag-input {
+    box-sizing: border-box;
+    width: 100%;
+    margin: 0;
+    font-size: 0.875rem;
+    font-family: inherit;
+    background-color: transparent;
+    color: var(--text-primary-color);
+    border: 0;
+    outline: none;
+    height: var(--default-button-size);
+    border: 1px solid var(--default-border-color);
+    border-left: 0;
+    letter-spacing: 0.4px;
+
+    :root[dir="ltr"] & {
+      border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
+    }
+
+    :root[dir="rtl"] & {
+      border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
+      border-left: 1px solid var(--default-border-color);
+      border-right: 0;
+    }
+
+    padding: 0.5rem;
+    padding-left: 0.25rem;
+    appearance: none;
+
+    &:focus-visible {
+      box-shadow: none;
+    }
+  }
+}

+ 176 - 0
packages/excalidraw/components/Stats/DragInput.tsx

@@ -0,0 +1,176 @@
+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 { deepCopyElement } from "../../element/newElement";
+
+import "./DragInput.scss";
+import clsx from "clsx";
+
+export type DragInputCallbackType = (
+  accumulatedChange: number,
+  instantChange: number,
+  stateAtStart: ExcalidrawElement,
+  shouldKeepAspectRatio: boolean,
+  shouldChangeByStepSize: boolean,
+  nextValue?: number,
+) => void;
+
+interface StatsDragInputProps {
+  label: string | React.ReactNode;
+  value: number;
+  element: ExcalidrawElement;
+  editable?: boolean;
+  shouldKeepAspectRatio?: boolean;
+  dragInputCallback: DragInputCallbackType;
+}
+
+const StatsDragInput = ({
+  label,
+  dragInputCallback,
+  value,
+  element,
+  editable = true,
+  shouldKeepAspectRatio,
+}: StatsDragInputProps) => {
+  const inputRef = useRef<HTMLInputElement>(null);
+  const labelRef = useRef<HTMLDivElement>(null);
+
+  const cbThrottled = useMemo(() => {
+    return throttle(dragInputCallback, 16);
+  }, [dragInputCallback]);
+
+  const [inputValue, setInputValue] = useState(value.toString());
+
+  useEffect(() => {
+    setInputValue(value.toString());
+  }, [value]);
+
+  return (
+    <div className={clsx("drag-input-container", !editable && "disabled")}>
+      <div
+        className="drag-input-label"
+        ref={labelRef}
+        onPointerDown={(event) => {
+          if (inputRef.current && editable) {
+            let startValue = Number(inputRef.current.value);
+            if (isNaN(startValue)) {
+              startValue = 0;
+            }
+
+            let lastPointer: {
+              x: number;
+              y: number;
+            } | null = null;
+
+            let stateAtStart: ExcalidrawElement | null = null;
+
+            let accumulatedChange: number | null = null;
+
+            document.body.classList.add("dragResize");
+
+            const onPointerMove = (event: PointerEvent) => {
+              if (!stateAtStart) {
+                stateAtStart = deepCopyElement(element);
+              }
+
+              if (!accumulatedChange) {
+                accumulatedChange = 0;
+              }
+
+              if (lastPointer && stateAtStart && accumulatedChange !== null) {
+                const instantChange = event.clientX - lastPointer.x;
+                accumulatedChange += instantChange;
+
+                cbThrottled(
+                  accumulatedChange,
+                  instantChange,
+                  stateAtStart,
+                  shouldKeepAspectRatio!!,
+                  event.shiftKey,
+                );
+              }
+
+              lastPointer = {
+                x: event.clientX,
+                y: event.clientY,
+              };
+            };
+
+            window.addEventListener(EVENT.POINTER_MOVE, onPointerMove, false);
+            window.addEventListener(
+              EVENT.POINTER_UP,
+              () => {
+                window.removeEventListener(
+                  EVENT.POINTER_MOVE,
+                  onPointerMove,
+                  false,
+                );
+
+                lastPointer = null;
+                accumulatedChange = null;
+                stateAtStart = null;
+
+                document.body.classList.remove("dragResize");
+              },
+              false,
+            );
+          }
+        }}
+        onPointerEnter={() => {
+          if (labelRef.current) {
+            labelRef.current.style.cursor = "ew-resize";
+          }
+        }}
+      >
+        {label}
+      </div>
+      <input
+        className="drag-input"
+        autoComplete="off"
+        spellCheck="false"
+        onKeyDown={(event) => {
+          if (editable) {
+            const eventTarget = event.target;
+
+            if (
+              eventTarget instanceof HTMLInputElement &&
+              event.key === KEYS.ENTER
+            ) {
+              const v = Number(eventTarget.value);
+              if (isNaN(v)) {
+                setInputValue(value.toString());
+                return;
+              }
+              dragInputCallback(
+                0,
+                0,
+                element,
+                shouldKeepAspectRatio!!,
+                false,
+                v,
+              );
+            }
+          }
+        }}
+        ref={inputRef}
+        value={inputValue}
+        onChange={(event) => {
+          const eventTarget = event.target;
+          if (eventTarget instanceof HTMLInputElement) {
+            setInputValue(event.target.value);
+          }
+        }}
+        onBlur={() => {
+          if (!inputValue) {
+            setInputValue(value.toString());
+          }
+        }}
+        disabled={!editable}
+      ></input>
+    </div>
+  );
+};
+
+export default StatsDragInput;

+ 4 - 9
packages/excalidraw/components/Stats.scss → packages/excalidraw/components/Stats/index.scss

@@ -1,4 +1,4 @@
-@import "../css/variables.module.scss";
+@import "../../css/variables.module.scss";
 
 .excalidraw {
   .Stats {
@@ -24,18 +24,13 @@
     }
 
     .statsItem {
+      width: 100%;
       margin-bottom: 4px;
-      display: flex;
-      align-items: center;
-      // margin-right: 8px;
+      display: grid;
+      gap: 4px;
 
       .label {
         margin-right: 4px;
-        width: 10px;
-      }
-
-      .input {
-        width: 55px;
       }
     }
 

+ 24 - 48
packages/excalidraw/components/Stats.tsx → packages/excalidraw/components/Stats/index.tsx

@@ -1,17 +1,19 @@
 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 type Scene from "../scene/Scene";
-import type { AppState, ExcalidrawProps } from "../types";
-import { CloseIcon } from "./icons";
-import { Island } from "./Island";
-import "./Stats.scss";
+import { getCommonBounds } from "../../element/bounds";
+import type { NonDeletedExcalidrawElement } from "../../element/types";
+import { t } from "../../i18n";
+import { getTargetElements } from "../../scene";
+import type Scene from "../../scene/Scene";
+import type { AppState, ExcalidrawProps } from "../../types";
+import { CloseIcon } from "../icons";
+import { Island } from "../Island";
 import { throttle } from "lodash";
-import DragInput from "./DragInput";
+import Dimension from "./Dimension";
+import Angle from "./Angle";
+
+import "./index.scss";
+import FontSize from "./FontSize";
 
-const STATS_TIMEOUT = 50;
 interface StatsProps {
   appState: AppState;
   scene: Scene;
@@ -19,14 +21,11 @@ interface StatsProps {
   onClose: () => void;
   renderCustomStats: ExcalidrawProps["renderCustomStats"];
 }
-
-type ElementStatItem = {
-  label: string;
-  property: "x" | "y" | "width" | "height" | "angle";
-};
+const STATS_TIMEOUT = 50;
 
 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);
 
@@ -106,39 +105,16 @@ export const Stats = (props: StatsProps) => {
                 {t(`element.${singleElement.type}`)}
               </div>
 
-              <div
-                style={{
-                  display: "grid",
-                  gridTemplateColumns: "repeat(2, 1fr)",
-                  gap: "4px 8px",
-                }}
-              >
-                {(
-                  [
-                    {
-                      label: "W",
-                      property: "width",
-                    },
-                    {
-                      label: "H",
-                      property: "height",
-                    },
-                    {
-                      label: "A",
-                      property: "angle",
-                    },
-                  ] as ElementStatItem[]
-                ).map((statsItem) => (
-                  <DragInput
-                    key={statsItem.label}
-                    label={statsItem.label}
-                    property={statsItem.property}
-                    element={singleElement}
-                    elementsMap={props.scene.getNonDeletedElementsMap()}
-                    zoom={props.appState.zoom}
-                  />
-                ))}
+              <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>
         )}

+ 23 - 0
packages/excalidraw/components/Stats/utils.ts

@@ -0,0 +1,23 @@
+import { isFrameLikeElement, isTextElement } from "../../element/typeChecks";
+import type { ExcalidrawElement } from "../../element/types";
+
+export const isPropertyEditable = (
+  element: ExcalidrawElement,
+  property: keyof ExcalidrawElement,
+) => {
+  if (property === "height" && isTextElement(element)) {
+    return false;
+  }
+  if (property === "width" && isTextElement(element)) {
+    return false;
+  }
+  if (property === "angle" && isFrameLikeElement(element)) {
+    return false;
+  }
+  return true;
+};
+
+export const getStepSizedValue = (value: number, stepSize: number) => {
+  const v = value + stepSize / 2;
+  return v - (v % stepSize);
+};