mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-03 20:34:40 +01:00 
			
		
		
		
	fix: resize multiple elements from center (#5560)
Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
		@@ -18,6 +18,7 @@ import { rescalePoints } from "../points";
 | 
			
		||||
 | 
			
		||||
// x and y position of top left corner, x and y position of bottom right corner
 | 
			
		||||
export type Bounds = readonly [number, number, number, number];
 | 
			
		||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
 | 
			
		||||
 | 
			
		||||
// If the element is created from right to left, the width is going to be negative
 | 
			
		||||
// This set of functions retrieves the absolute position of the 4 points.
 | 
			
		||||
@@ -68,11 +69,95 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
 | 
			
		||||
  return shape.sets[0].ops;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
 | 
			
		||||
const getBezierValueForT = (
 | 
			
		||||
  t: number,
 | 
			
		||||
  p0: number,
 | 
			
		||||
  p1: number,
 | 
			
		||||
  p2: number,
 | 
			
		||||
  p3: number,
 | 
			
		||||
) => {
 | 
			
		||||
  const oneMinusT = 1 - t;
 | 
			
		||||
  return (
 | 
			
		||||
    Math.pow(oneMinusT, 3) * p0 +
 | 
			
		||||
    3 * Math.pow(oneMinusT, 2) * t * p1 +
 | 
			
		||||
    3 * oneMinusT * Math.pow(t, 2) * p2 +
 | 
			
		||||
    Math.pow(t, 3) * p3
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const solveQuadratic = (
 | 
			
		||||
  p0: number,
 | 
			
		||||
  p1: number,
 | 
			
		||||
  p2: number,
 | 
			
		||||
  p3: number,
 | 
			
		||||
): MaybeQuadraticSolution => {
 | 
			
		||||
  const i = p1 - p0;
 | 
			
		||||
  const j = p2 - p1;
 | 
			
		||||
  const k = p3 - p2;
 | 
			
		||||
 | 
			
		||||
  const a = 3 * i - 6 * j + 3 * k;
 | 
			
		||||
  const b = 6 * j - 6 * i;
 | 
			
		||||
  const c = 3 * i;
 | 
			
		||||
 | 
			
		||||
  const sqrtPart = b * b - 4 * a * c;
 | 
			
		||||
  const hasSolution = sqrtPart >= 0;
 | 
			
		||||
 | 
			
		||||
  if (!hasSolution) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
 | 
			
		||||
  const t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
 | 
			
		||||
 | 
			
		||||
  let s1 = null;
 | 
			
		||||
  let s2 = null;
 | 
			
		||||
 | 
			
		||||
  if (t1 >= 0 && t1 <= 1) {
 | 
			
		||||
    s1 = getBezierValueForT(t1, p0, p1, p2, p3);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (t2 >= 0 && t2 <= 1) {
 | 
			
		||||
    s2 = getBezierValueForT(t2, p0, p1, p2, p3);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [s1, s2];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getCubicBezierCurveBound = (
 | 
			
		||||
  p0: Point,
 | 
			
		||||
  p1: Point,
 | 
			
		||||
  p2: Point,
 | 
			
		||||
  p3: Point,
 | 
			
		||||
): Bounds => {
 | 
			
		||||
  const solX = solveQuadratic(p0[0], p1[0], p2[0], p3[0]);
 | 
			
		||||
  const solY = solveQuadratic(p0[1], p1[1], p2[1], p3[1]);
 | 
			
		||||
 | 
			
		||||
  let minX = Math.min(p0[0], p3[0]);
 | 
			
		||||
  let maxX = Math.max(p0[0], p3[0]);
 | 
			
		||||
 | 
			
		||||
  if (solX) {
 | 
			
		||||
    const xs = solX.filter((x) => x !== null) as number[];
 | 
			
		||||
    minX = Math.min(minX, ...xs);
 | 
			
		||||
    maxX = Math.max(maxX, ...xs);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let minY = Math.min(p0[1], p3[1]);
 | 
			
		||||
  let maxY = Math.max(p0[1], p3[1]);
 | 
			
		||||
  if (solY) {
 | 
			
		||||
    const ys = solY.filter((y) => y !== null) as number[];
 | 
			
		||||
    minY = Math.min(minY, ...ys);
 | 
			
		||||
    maxY = Math.max(maxY, ...ys);
 | 
			
		||||
  }
 | 
			
		||||
  return [minX, minY, maxX, maxY];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getMinMaxXYFromCurvePathOps = (
 | 
			
		||||
  ops: Op[],
 | 
			
		||||
  transformXY?: (x: number, y: number) => [number, number],
 | 
			
		||||
): [number, number, number, number] => {
 | 
			
		||||
  let currentP: Point = [0, 0];
 | 
			
		||||
 | 
			
		||||
  const { minX, minY, maxX, maxY } = ops.reduce(
 | 
			
		||||
    (limits, { op, data }) => {
 | 
			
		||||
      // There are only four operation types:
 | 
			
		||||
@@ -83,38 +168,29 @@ const getMinMaxXYFromCurvePathOps = (
 | 
			
		||||
        // move operation does not draw anything; so, it always
 | 
			
		||||
        // returns false
 | 
			
		||||
      } else if (op === "bcurveTo") {
 | 
			
		||||
        // create points from bezier curve
 | 
			
		||||
        // bezier curve stores data as a flattened array of three positions
 | 
			
		||||
        // [x1, y1, x2, y2, x3, y3]
 | 
			
		||||
        const p1 = [data[0], data[1]] as Point;
 | 
			
		||||
        const p2 = [data[2], data[3]] as Point;
 | 
			
		||||
        const p3 = [data[4], data[5]] as Point;
 | 
			
		||||
        const _p1 = [data[0], data[1]] as Point;
 | 
			
		||||
        const _p2 = [data[2], data[3]] as Point;
 | 
			
		||||
        const _p3 = [data[4], data[5]] as Point;
 | 
			
		||||
 | 
			
		||||
        const p0 = currentP;
 | 
			
		||||
        currentP = p3;
 | 
			
		||||
        const p1 = transformXY ? transformXY(..._p1) : _p1;
 | 
			
		||||
        const p2 = transformXY ? transformXY(..._p2) : _p2;
 | 
			
		||||
        const p3 = transformXY ? transformXY(..._p3) : _p3;
 | 
			
		||||
 | 
			
		||||
        const equation = (t: number, idx: number) =>
 | 
			
		||||
          Math.pow(1 - t, 3) * p3[idx] +
 | 
			
		||||
          3 * t * Math.pow(1 - t, 2) * p2[idx] +
 | 
			
		||||
          3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
 | 
			
		||||
          p0[idx] * Math.pow(t, 3);
 | 
			
		||||
        const p0 = transformXY ? transformXY(...currentP) : currentP;
 | 
			
		||||
        currentP = _p3;
 | 
			
		||||
 | 
			
		||||
        let t = 0;
 | 
			
		||||
        while (t <= 1.0) {
 | 
			
		||||
          let x = equation(t, 0);
 | 
			
		||||
          let y = equation(t, 1);
 | 
			
		||||
          if (transformXY) {
 | 
			
		||||
            [x, y] = transformXY(x, y);
 | 
			
		||||
          }
 | 
			
		||||
        const [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
 | 
			
		||||
          p0,
 | 
			
		||||
          p1,
 | 
			
		||||
          p2,
 | 
			
		||||
          p3,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
          limits.minY = Math.min(limits.minY, y);
 | 
			
		||||
          limits.minX = Math.min(limits.minX, x);
 | 
			
		||||
        limits.minX = Math.min(limits.minX, minX);
 | 
			
		||||
        limits.minY = Math.min(limits.minY, minY);
 | 
			
		||||
 | 
			
		||||
          limits.maxX = Math.max(limits.maxX, x);
 | 
			
		||||
          limits.maxY = Math.max(limits.maxY, y);
 | 
			
		||||
 | 
			
		||||
          t += 0.1;
 | 
			
		||||
        }
 | 
			
		||||
        limits.maxX = Math.max(limits.maxX, maxX);
 | 
			
		||||
        limits.maxY = Math.max(limits.maxY, maxY);
 | 
			
		||||
      } else if (op === "lineTo") {
 | 
			
		||||
        // TODO: Implement this
 | 
			
		||||
      } else if (op === "qcurveTo") {
 | 
			
		||||
@@ -124,7 +200,6 @@ const getMinMaxXYFromCurvePathOps = (
 | 
			
		||||
    },
 | 
			
		||||
    { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return [minX, minY, maxX, maxY];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import {
 | 
			
		||||
  getElementAbsoluteCoords,
 | 
			
		||||
  getCommonBounds,
 | 
			
		||||
  getResizedElementAbsoluteCoords,
 | 
			
		||||
  getCommonBoundingBox,
 | 
			
		||||
} from "./bounds";
 | 
			
		||||
import {
 | 
			
		||||
  isFreeDrawElement,
 | 
			
		||||
@@ -137,8 +138,10 @@ export const transformElements = (
 | 
			
		||||
      transformHandleType === "se"
 | 
			
		||||
    ) {
 | 
			
		||||
      resizeMultipleElements(
 | 
			
		||||
        pointerDownState,
 | 
			
		||||
        selectedElements,
 | 
			
		||||
        transformHandleType,
 | 
			
		||||
        shouldResizeFromCenter,
 | 
			
		||||
        pointerX,
 | 
			
		||||
        pointerY,
 | 
			
		||||
      );
 | 
			
		||||
@@ -637,146 +640,142 @@ export const resizeSingleElement = (
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const resizeMultipleElements = (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
  pointerDownState: PointerDownState,
 | 
			
		||||
  selectedElements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
  transformHandleType: "nw" | "ne" | "sw" | "se",
 | 
			
		||||
  shouldResizeFromCenter: boolean,
 | 
			
		||||
  pointerX: number,
 | 
			
		||||
  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 (transformHandleType) {
 | 
			
		||||
    case "se":
 | 
			
		||||
      scale = Math.max(
 | 
			
		||||
        (pointerX - x1) / (x2 - x1),
 | 
			
		||||
        (pointerY - y1) / (y2 - y1),
 | 
			
		||||
      );
 | 
			
		||||
      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":
 | 
			
		||||
      scale = Math.max(
 | 
			
		||||
        (x2 - pointerX) / (x2 - x1),
 | 
			
		||||
        (y2 - pointerY) / (y2 - y1),
 | 
			
		||||
      );
 | 
			
		||||
      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":
 | 
			
		||||
      scale = Math.max(
 | 
			
		||||
        (pointerX - x1) / (x2 - x1),
 | 
			
		||||
        (y2 - pointerY) / (y2 - y1),
 | 
			
		||||
      );
 | 
			
		||||
      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":
 | 
			
		||||
      scale = Math.max(
 | 
			
		||||
        (x2 - pointerX) / (x2 - x1),
 | 
			
		||||
        (pointerY - y1) / (y2 - y1),
 | 
			
		||||
      );
 | 
			
		||||
      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;
 | 
			
		||||
  // map selected elements to the original elements. While it never should
 | 
			
		||||
  // happen that pointerDownState.originalElements won't contain the selected
 | 
			
		||||
  // elements during resize, this coupling isn't guaranteed, so to ensure
 | 
			
		||||
  // type safety we need to transform only those elements we filter.
 | 
			
		||||
  const targetElements = selectedElements.reduce(
 | 
			
		||||
    (
 | 
			
		||||
      acc: {
 | 
			
		||||
        /** element at resize start */
 | 
			
		||||
        orig: NonDeletedExcalidrawElement;
 | 
			
		||||
        /** latest element */
 | 
			
		||||
        latest: NonDeletedExcalidrawElement;
 | 
			
		||||
      }[],
 | 
			
		||||
      element,
 | 
			
		||||
    ) => {
 | 
			
		||||
      const origElement = pointerDownState.originalElements.get(element.id);
 | 
			
		||||
      if (origElement) {
 | 
			
		||||
        acc.push({ orig: origElement, latest: element });
 | 
			
		||||
      }
 | 
			
		||||
      return acc;
 | 
			
		||||
    },
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
 | 
			
		||||
    targetElements.map(({ orig }) => orig),
 | 
			
		||||
  );
 | 
			
		||||
  const direction = transformHandleType;
 | 
			
		||||
 | 
			
		||||
  const mapDirectionsToAnchors: Record<typeof direction, Point> = {
 | 
			
		||||
    ne: [minX, maxY],
 | 
			
		||||
    se: [minX, minY],
 | 
			
		||||
    sw: [maxX, minY],
 | 
			
		||||
    nw: [maxX, maxY],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // anchor point must be on the opposite side of the dragged selection handle
 | 
			
		||||
  // or be the center of the selection if alt is pressed
 | 
			
		||||
  const [anchorX, anchorY]: Point = shouldResizeFromCenter
 | 
			
		||||
    ? [midX, midY]
 | 
			
		||||
    : mapDirectionsToAnchors[direction];
 | 
			
		||||
 | 
			
		||||
  const mapDirectionsToPointerSides: Record<
 | 
			
		||||
    typeof direction,
 | 
			
		||||
    [x: boolean, y: boolean]
 | 
			
		||||
  > = {
 | 
			
		||||
    ne: [pointerX >= anchorX, pointerY <= anchorY],
 | 
			
		||||
    se: [pointerX >= anchorX, pointerY >= anchorY],
 | 
			
		||||
    sw: [pointerX <= anchorX, pointerY >= anchorY],
 | 
			
		||||
    nw: [pointerX <= anchorX, pointerY <= anchorY],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // pointer side relative to anchor
 | 
			
		||||
  const [pointerSideX, pointerSideY] = mapDirectionsToPointerSides[
 | 
			
		||||
    direction
 | 
			
		||||
  ].map((condition) => (condition ? 1 : -1));
 | 
			
		||||
 | 
			
		||||
  // stop resizing if a pointer is on the other side of selection
 | 
			
		||||
  if (pointerSideX < 0 && pointerSideY < 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (scale > 0) {
 | 
			
		||||
    const updates = elements.reduce(
 | 
			
		||||
      (prev, element) => {
 | 
			
		||||
        if (!prev) {
 | 
			
		||||
          return prev;
 | 
			
		||||
 | 
			
		||||
  const scale =
 | 
			
		||||
    Math.max(
 | 
			
		||||
      (pointerSideX * Math.abs(pointerX - anchorX)) / (maxX - minX),
 | 
			
		||||
      (pointerSideY * Math.abs(pointerY - anchorY)) / (maxY - minY),
 | 
			
		||||
    ) * (shouldResizeFromCenter ? 2 : 1);
 | 
			
		||||
 | 
			
		||||
  if (scale === 1) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  targetElements.forEach((element) => {
 | 
			
		||||
    const width = element.orig.width * scale;
 | 
			
		||||
    const height = element.orig.height * scale;
 | 
			
		||||
    const x = anchorX + (element.orig.x - anchorX) * scale;
 | 
			
		||||
    const y = anchorY + (element.orig.y - anchorY) * scale;
 | 
			
		||||
 | 
			
		||||
    // readjust points for linear & free draw elements
 | 
			
		||||
    const rescaledPoints = rescalePointsInElement(element.orig, width, height);
 | 
			
		||||
 | 
			
		||||
    const update: {
 | 
			
		||||
      width: number;
 | 
			
		||||
      height: number;
 | 
			
		||||
      x: number;
 | 
			
		||||
      y: number;
 | 
			
		||||
      points?: Point[];
 | 
			
		||||
      fontSize?: number;
 | 
			
		||||
      baseline?: number;
 | 
			
		||||
    } = {
 | 
			
		||||
      width,
 | 
			
		||||
      height,
 | 
			
		||||
      x,
 | 
			
		||||
      y,
 | 
			
		||||
      ...rescaledPoints,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
 | 
			
		||||
 | 
			
		||||
    const boundTextElement = getBoundTextElement(element.latest);
 | 
			
		||||
 | 
			
		||||
    if (boundTextElement || isTextElement(element.orig)) {
 | 
			
		||||
      const optionalPadding = boundTextElement ? BOUND_TEXT_PADDING * 2 : 0;
 | 
			
		||||
      const textMeasurements = measureFontSizeFromWH(
 | 
			
		||||
        boundTextElement ?? (element.orig as ExcalidrawTextElement),
 | 
			
		||||
        width - optionalPadding,
 | 
			
		||||
        height - optionalPadding,
 | 
			
		||||
      );
 | 
			
		||||
      if (textMeasurements) {
 | 
			
		||||
        if (isTextElement(element.orig)) {
 | 
			
		||||
          update.fontSize = textMeasurements.size;
 | 
			
		||||
          update.baseline = textMeasurements.baseline;
 | 
			
		||||
        }
 | 
			
		||||
        const width = element.width * scale;
 | 
			
		||||
        const height = element.height * scale;
 | 
			
		||||
        const boundTextElement = getBoundTextElement(element);
 | 
			
		||||
        let font: { fontSize?: number; baseline?: number } = {};
 | 
			
		||||
 | 
			
		||||
        if (boundTextElement) {
 | 
			
		||||
          const nextFont = measureFontSizeFromWH(
 | 
			
		||||
            boundTextElement,
 | 
			
		||||
            width - BOUND_TEXT_PADDING * 2,
 | 
			
		||||
            height - BOUND_TEXT_PADDING * 2,
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (nextFont === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
          }
 | 
			
		||||
          font = {
 | 
			
		||||
            fontSize: nextFont.size,
 | 
			
		||||
            baseline: nextFont.baseline,
 | 
			
		||||
          boundTextUpdates = {
 | 
			
		||||
            fontSize: textMeasurements.size,
 | 
			
		||||
            baseline: textMeasurements.baseline,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isTextElement(element)) {
 | 
			
		||||
          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);
 | 
			
		||||
 | 
			
		||||
        updateBoundElements(element, {
 | 
			
		||||
          newSize: { width, height },
 | 
			
		||||
          simultaneouslyUpdated: elements,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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]);
 | 
			
		||||
        const boundTextElement = getBoundTextElement(element);
 | 
			
		||||
 | 
			
		||||
        if (boundTextElement) {
 | 
			
		||||
          mutateElement(boundTextElement, {
 | 
			
		||||
            fontSize: updates[index].fontSize,
 | 
			
		||||
            baseline: updates[index].baseline,
 | 
			
		||||
          });
 | 
			
		||||
          handleBindTextResize(element, transformHandleType);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    mutateElement(element.latest, update);
 | 
			
		||||
 | 
			
		||||
    if (boundTextElement && boundTextUpdates) {
 | 
			
		||||
      mutateElement(boundTextElement, boundTextUpdates);
 | 
			
		||||
      handleBindTextResize(element.latest, transformHandleType);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const rotateMultipleElements = (
 | 
			
		||||
 
 | 
			
		||||
@@ -9,46 +9,22 @@ export const getSizeFromPoints = (points: readonly Point[]) => {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @arg dimension, 0 for rescaling only x, 1 for y */
 | 
			
		||||
export const rescalePoints = (
 | 
			
		||||
  dimension: 0 | 1,
 | 
			
		||||
  nextDimensionSize: number,
 | 
			
		||||
  prevPoints: readonly Point[],
 | 
			
		||||
  newSize: number,
 | 
			
		||||
  points: readonly Point[],
 | 
			
		||||
): Point[] => {
 | 
			
		||||
  const prevDimValues = prevPoints.map((point) => point[dimension]);
 | 
			
		||||
  const prevMaxDimension = Math.max(...prevDimValues);
 | 
			
		||||
  const prevMinDimension = Math.min(...prevDimValues);
 | 
			
		||||
  const prevDimensionSize = prevMaxDimension - prevMinDimension;
 | 
			
		||||
  const coordinates = points.map((point) => point[dimension]);
 | 
			
		||||
  const maxCoordinate = Math.max(...coordinates);
 | 
			
		||||
  const minCoordinate = Math.min(...coordinates);
 | 
			
		||||
  const size = maxCoordinate - minCoordinate;
 | 
			
		||||
  const scale = size === 0 ? 1 : newSize / size;
 | 
			
		||||
 | 
			
		||||
  const dimensionScaleFactor =
 | 
			
		||||
    prevDimensionSize === 0 ? 1 : nextDimensionSize / prevDimensionSize;
 | 
			
		||||
 | 
			
		||||
  let nextMinDimension = Infinity;
 | 
			
		||||
 | 
			
		||||
  const scaledPoints = prevPoints.map(
 | 
			
		||||
    (prevPoint) =>
 | 
			
		||||
      prevPoint.map((value, currentDimension) => {
 | 
			
		||||
        if (currentDimension !== dimension) {
 | 
			
		||||
          return value;
 | 
			
		||||
        }
 | 
			
		||||
        const scaledValue = value * dimensionScaleFactor;
 | 
			
		||||
        nextMinDimension = Math.min(scaledValue, nextMinDimension);
 | 
			
		||||
        return scaledValue;
 | 
			
		||||
      }) as [number, number],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (scaledPoints.length === 2) {
 | 
			
		||||
    // we don't translate two-point lines
 | 
			
		||||
    return scaledPoints;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const translation = prevMinDimension - nextMinDimension;
 | 
			
		||||
 | 
			
		||||
  const nextPoints = scaledPoints.map(
 | 
			
		||||
    (scaledPoint) =>
 | 
			
		||||
      scaledPoint.map((value, currentDimension) => {
 | 
			
		||||
        return currentDimension === dimension ? value + translation : value;
 | 
			
		||||
      }) as [number, number],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return nextPoints;
 | 
			
		||||
  return points.map((point): Point => {
 | 
			
		||||
    const newCoordinate = point[dimension] * scale;
 | 
			
		||||
    const newPoint = [...point];
 | 
			
		||||
    newPoint[dimension] = newCoordinate;
 | 
			
		||||
    return newPoint as unknown as Point;
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user