ソースを参照

feat: wrap texts from stats panel (#9552)

Ryan Di 2 ヶ月 前
コミット
93c92d13e9

+ 56 - 187
packages/element/src/resizeElements.ts

@@ -2,7 +2,6 @@ import {
   pointCenter,
   normalizeRadians,
   pointFrom,
-  pointFromPair,
   pointRotateRads,
   type Radians,
   type LocalPoint,
@@ -104,18 +103,6 @@ export const transformElements = (
         );
         updateBoundElements(element, scene);
       }
-    } else if (isTextElement(element) && transformHandleType) {
-      resizeSingleTextElement(
-        originalElements,
-        element,
-        scene,
-        transformHandleType,
-        shouldResizeFromCenter,
-        pointerX,
-        pointerY,
-      );
-      updateBoundElements(element, scene);
-      return true;
     } else if (transformHandleType) {
       const elementId = selectedElements[0].id;
       const latestElement = elementsMap.get(elementId);
@@ -150,6 +137,9 @@ export const transformElements = (
         );
       }
     }
+    if (isTextElement(element)) {
+      updateBoundElements(element, scene);
+    }
     return true;
   } else if (selectedElements.length > 1) {
     if (transformHandleType === "rotation") {
@@ -282,151 +272,50 @@ export const measureFontSizeFromWidth = (
   };
 };
 
-const resizeSingleTextElement = (
-  originalElements: PointerDownState["originalElements"],
+export const resizeSingleTextElement = (
+  origElement: NonDeleted<ExcalidrawTextElement>,
   element: NonDeleted<ExcalidrawTextElement>,
   scene: Scene,
   transformHandleType: TransformHandleDirection,
   shouldResizeFromCenter: boolean,
-  pointerX: number,
-  pointerY: number,
+  nextWidth: number,
+  nextHeight: number,
 ) => {
   const elementsMap = scene.getNonDeletedElementsMap();
-  const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
-    element,
-    elementsMap,
-  );
-  // rotation pointer with reverse angle
-  const [rotatedX, rotatedY] = pointRotateRads(
-    pointFrom(pointerX, pointerY),
-    pointFrom(cx, cy),
-    -element.angle as Radians,
-  );
-  let scaleX = 0;
-  let scaleY = 0;
 
-  if (transformHandleType !== "e" && transformHandleType !== "w") {
-    if (transformHandleType.includes("e")) {
-      scaleX = (rotatedX - x1) / (x2 - x1);
-    }
-    if (transformHandleType.includes("w")) {
-      scaleX = (x2 - rotatedX) / (x2 - x1);
-    }
-    if (transformHandleType.includes("n")) {
-      scaleY = (y2 - rotatedY) / (y2 - y1);
-    }
-    if (transformHandleType.includes("s")) {
-      scaleY = (rotatedY - y1) / (y2 - y1);
-    }
-  }
+  const metricsWidth = element.width * (nextHeight / element.height);
 
-  const scale = Math.max(scaleX, scaleY);
-
-  if (scale > 0) {
-    const nextWidth = element.width * scale;
-    const nextHeight = element.height * scale;
-    const metrics = measureFontSizeFromWidth(element, elementsMap, nextWidth);
-    if (metrics === null) {
-      return;
-    }
-
-    const startTopLeft = [x1, y1];
-    const startBottomRight = [x2, y2];
-    const startCenter = [cx, cy];
-
-    let newTopLeft = pointFrom<GlobalPoint>(x1, y1);
-    if (["n", "w", "nw"].includes(transformHandleType)) {
-      newTopLeft = pointFrom<GlobalPoint>(
-        startBottomRight[0] - Math.abs(nextWidth),
-        startBottomRight[1] - Math.abs(nextHeight),
-      );
-    }
-    if (transformHandleType === "ne") {
-      const bottomLeft = [startTopLeft[0], startBottomRight[1]];
-      newTopLeft = pointFrom<GlobalPoint>(
-        bottomLeft[0],
-        bottomLeft[1] - Math.abs(nextHeight),
-      );
-    }
-    if (transformHandleType === "sw") {
-      const topRight = [startBottomRight[0], startTopLeft[1]];
-      newTopLeft = pointFrom<GlobalPoint>(
-        topRight[0] - Math.abs(nextWidth),
-        topRight[1],
-      );
-    }
-
-    if (["s", "n"].includes(transformHandleType)) {
-      newTopLeft[0] = startCenter[0] - nextWidth / 2;
-    }
-    if (["e", "w"].includes(transformHandleType)) {
-      newTopLeft[1] = startCenter[1] - nextHeight / 2;
-    }
-
-    if (shouldResizeFromCenter) {
-      newTopLeft[0] = startCenter[0] - Math.abs(nextWidth) / 2;
-      newTopLeft[1] = startCenter[1] - Math.abs(nextHeight) / 2;
-    }
+  const metrics = measureFontSizeFromWidth(element, elementsMap, metricsWidth);
+  if (metrics === null) {
+    return;
+  }
 
-    const angle = element.angle;
-    const rotatedTopLeft = pointRotateRads(
-      newTopLeft,
-      pointFrom(cx, cy),
-      angle,
-    );
-    const newCenter = pointFrom<GlobalPoint>(
-      newTopLeft[0] + Math.abs(nextWidth) / 2,
-      newTopLeft[1] + Math.abs(nextHeight) / 2,
+  if (transformHandleType.includes("n") || transformHandleType.includes("s")) {
+    const previousOrigin = pointFrom<GlobalPoint>(origElement.x, origElement.y);
+
+    const newOrigin = getResizedOrigin(
+      previousOrigin,
+      origElement.width,
+      origElement.height,
+      metricsWidth,
+      nextHeight,
+      origElement.angle,
+      transformHandleType,
+      false,
+      shouldResizeFromCenter,
     );
-    const rotatedNewCenter = pointRotateRads(
-      newCenter,
-      pointFrom(cx, cy),
-      angle,
-    );
-    newTopLeft = pointRotateRads(
-      rotatedTopLeft,
-      rotatedNewCenter,
-      -angle as Radians,
-    );
-    const [nextX, nextY] = newTopLeft;
 
     scene.mutateElement(element, {
       fontSize: metrics.size,
-      width: nextWidth,
+      width: metricsWidth,
       height: nextHeight,
-      x: nextX,
-      y: nextY,
+      x: newOrigin.x,
+      y: newOrigin.y,
     });
+    return;
   }
 
   if (transformHandleType === "e" || transformHandleType === "w") {
-    const stateAtResizeStart = originalElements.get(element.id)!;
-    const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
-      stateAtResizeStart,
-      stateAtResizeStart.width,
-      stateAtResizeStart.height,
-      true,
-    );
-    const startTopLeft = pointFrom<GlobalPoint>(x1, y1);
-    const startBottomRight = pointFrom<GlobalPoint>(x2, y2);
-    const startCenter = pointCenter(startTopLeft, startBottomRight);
-
-    const rotatedPointer = pointRotateRads(
-      pointFrom(pointerX, pointerY),
-      startCenter,
-      -stateAtResizeStart.angle as Radians,
-    );
-
-    const [esx1, , esx2] = getResizedElementAbsoluteCoords(
-      element,
-      element.width,
-      element.height,
-      true,
-    );
-
-    const boundsCurrentWidth = esx2 - esx1;
-
-    const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
     const minWidth = getMinTextElementWidth(
       getFontString({
         fontSize: element.fontSize,
@@ -435,17 +324,7 @@ const resizeSingleTextElement = (
       element.lineHeight,
     );
 
-    let scaleX = atStartBoundsWidth / boundsCurrentWidth;
-
-    if (transformHandleType.includes("e")) {
-      scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
-    }
-    if (transformHandleType.includes("w")) {
-      scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
-    }
-
-    const newWidth =
-      element.width * scaleX < minWidth ? minWidth : element.width * scaleX;
+    const newWidth = Math.max(minWidth, nextWidth);
 
     const text = wrapText(
       element.originalText,
@@ -458,49 +337,27 @@ const resizeSingleTextElement = (
       element.lineHeight,
     );
 
-    const eleNewHeight = metrics.height;
+    const newHeight = metrics.height;
 
-    const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
-      getResizedElementAbsoluteCoords(
-        stateAtResizeStart,
-        newWidth,
-        eleNewHeight,
-        true,
-      );
-    const newBoundsWidth = newBoundsX2 - newBoundsX1;
-    const newBoundsHeight = newBoundsY2 - newBoundsY1;
-
-    let newTopLeft = [...startTopLeft] as [number, number];
-    if (["n", "w", "nw"].includes(transformHandleType)) {
-      newTopLeft = [
-        startBottomRight[0] - Math.abs(newBoundsWidth),
-        startTopLeft[1],
-      ];
-    }
+    const previousOrigin = pointFrom<GlobalPoint>(origElement.x, origElement.y);
 
-    // adjust topLeft to new rotation point
-    const angle = stateAtResizeStart.angle;
-    const rotatedTopLeft = pointRotateRads(
-      pointFromPair(newTopLeft),
-      startCenter,
-      angle,
-    );
-    const newCenter = pointFrom(
-      newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
-      newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
-    );
-    const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle);
-    newTopLeft = pointRotateRads(
-      rotatedTopLeft,
-      rotatedNewCenter,
-      -angle as Radians,
+    const newOrigin = getResizedOrigin(
+      previousOrigin,
+      origElement.width,
+      origElement.height,
+      newWidth,
+      newHeight,
+      element.angle,
+      transformHandleType,
+      false,
+      shouldResizeFromCenter,
     );
 
     const resizedElement: Partial<ExcalidrawTextElement> = {
       width: Math.abs(newWidth),
       height: Math.abs(metrics.height),
-      x: newTopLeft[0],
-      y: newTopLeft[1],
+      x: newOrigin.x,
+      y: newOrigin.y,
       text,
       autoResize: false,
     };
@@ -821,6 +678,18 @@ export const resizeSingleElement = (
     shouldInformMutation?: boolean;
   } = {},
 ) => {
+  if (isTextElement(latestElement) && isTextElement(origElement)) {
+    return resizeSingleTextElement(
+      origElement,
+      latestElement,
+      scene,
+      handleDirection,
+      shouldResizeFromCenter,
+      nextWidth,
+      nextHeight,
+    );
+  }
+
   let boundTextFont: { fontSize?: number } = {};
   const elementsMap = scene.getNonDeletedElementsMap();
   const boundTextElement = getBoundTextElement(latestElement, elementsMap);

+ 18 - 7
packages/excalidraw/components/Stats/stats.test.tsx

@@ -401,11 +401,23 @@ describe("stats for a non-generic element", () => {
     UI.updateInput(input, "36");
     expect(text.fontSize).toBe(36);
 
-    // cannot change width or height
-    const width = UI.queryStatsProperty("W")?.querySelector(".drag-input");
-    expect(width).toBeUndefined();
-    const height = UI.queryStatsProperty("H")?.querySelector(".drag-input");
-    expect(height).toBeUndefined();
+    // can change width or height
+    const width = UI.queryStatsProperty("W")?.querySelector(
+      ".drag-input",
+    ) as HTMLInputElement;
+    expect(width).toBeDefined();
+    const height = UI.queryStatsProperty("H")?.querySelector(
+      ".drag-input",
+    ) as HTMLInputElement;
+    expect(height).toBeDefined();
+
+    const textHeightBeforeWrapping = text.height;
+    const textBeforeWrapping = text.text;
+    const originalTextBeforeWrapping = textBeforeWrapping;
+    UI.updateInput(width, "30");
+    expect(text.height).toBeGreaterThan(textHeightBeforeWrapping);
+    expect(text.text).not.toBe(textBeforeWrapping);
+    expect(text.originalText).toBe(originalTextBeforeWrapping);
 
     // min font size is 4
     UI.updateInput(input, "0");
@@ -627,12 +639,11 @@ describe("stats for multiple elements", () => {
     ) as HTMLInputElement;
     expect(fontSize).toBeDefined();
 
-    // changing width does not affect text
     UI.updateInput(width, "200");
 
     expect(rectangle?.width).toBe(200);
     expect(frame.width).toBe(200);
-    expect(text?.width).not.toBe(200);
+    expect(text?.width).toBe(200);
 
     UI.updateInput(angle, "40");
 

+ 1 - 7
packages/excalidraw/components/Stats/utils.ts

@@ -1,7 +1,7 @@
 import { pointFrom, pointRotateRads } from "@excalidraw/math";
 
 import { getBoundTextElement } from "@excalidraw/element";
-import { isFrameLikeElement, isTextElement } from "@excalidraw/element";
+import { isFrameLikeElement } from "@excalidraw/element";
 
 import {
   getSelectedGroupIds,
@@ -41,12 +41,6 @@ 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;
   }