12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556 |
- import {
- pointCenter,
- normalizeRadians,
- pointFrom,
- pointFromPair,
- pointRotateRads,
- type Radians,
- type LocalPoint,
- } from "@excalidraw/math";
- import {
- MIN_FONT_SIZE,
- SHIFT_LOCKING_ANGLE,
- rescalePoints,
- getFontString,
- } from "@excalidraw/common";
- import type { GlobalPoint } from "@excalidraw/math";
- import type Scene from "@excalidraw/excalidraw/scene/Scene";
- import type { PointerDownState } from "@excalidraw/excalidraw/types";
- import type { Mutable } from "@excalidraw/common/utility-types";
- import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
- import {
- getElementAbsoluteCoords,
- getCommonBounds,
- getResizedElementAbsoluteCoords,
- getCommonBoundingBox,
- getElementBounds,
- } from "./bounds";
- import { LinearElementEditor } from "./linearElementEditor";
- import { mutateElement } from "./mutateElement";
- import {
- getBoundTextElement,
- getBoundTextElementId,
- getContainerElement,
- handleBindTextResize,
- getBoundTextMaxWidth,
- } from "./textElement";
- import {
- getMinTextElementWidth,
- measureText,
- getApproxMinLineWidth,
- getApproxMinLineHeight,
- } from "./textMeasurements";
- import { wrapText } from "./textWrapping";
- import {
- isArrowElement,
- isBoundToContainer,
- isElbowArrow,
- isFrameLikeElement,
- isFreeDrawElement,
- isImageElement,
- isLinearElement,
- isTextElement,
- } from "./typeChecks";
- import { isInGroup } from "./groups";
- import type { BoundingBox } from "./bounds";
- import type {
- MaybeTransformHandleType,
- TransformHandleDirection,
- } from "./transformHandles";
- import type {
- ExcalidrawLinearElement,
- ExcalidrawTextElement,
- NonDeletedExcalidrawElement,
- NonDeleted,
- ExcalidrawElement,
- ExcalidrawTextElementWithContainer,
- ExcalidrawImageElement,
- ElementsMap,
- SceneElementsMap,
- ExcalidrawElbowArrowElement,
- } from "./types";
- // Returns true when transform (resizing/rotation) happened
- export const transformElements = (
- originalElements: PointerDownState["originalElements"],
- transformHandleType: MaybeTransformHandleType,
- selectedElements: readonly NonDeletedExcalidrawElement[],
- elementsMap: SceneElementsMap,
- scene: Scene,
- shouldRotateWithDiscreteAngle: boolean,
- shouldResizeFromCenter: boolean,
- shouldMaintainAspectRatio: boolean,
- pointerX: number,
- pointerY: number,
- centerX: number,
- centerY: number,
- ): boolean => {
- if (selectedElements.length === 1) {
- const [element] = selectedElements;
- if (transformHandleType === "rotation") {
- if (!isElbowArrow(element)) {
- rotateSingleElement(
- element,
- elementsMap,
- scene,
- pointerX,
- pointerY,
- shouldRotateWithDiscreteAngle,
- );
- updateBoundElements(element, elementsMap);
- }
- } else if (isTextElement(element) && transformHandleType) {
- resizeSingleTextElement(
- originalElements,
- element,
- elementsMap,
- transformHandleType,
- shouldResizeFromCenter,
- pointerX,
- pointerY,
- );
- updateBoundElements(element, elementsMap);
- return true;
- } else if (transformHandleType) {
- const elementId = selectedElements[0].id;
- const latestElement = elementsMap.get(elementId);
- const origElement = originalElements.get(elementId);
- if (latestElement && origElement) {
- const { nextWidth, nextHeight } =
- getNextSingleWidthAndHeightFromPointer(
- latestElement,
- origElement,
- elementsMap,
- originalElements,
- transformHandleType,
- pointerX,
- pointerY,
- {
- shouldMaintainAspectRatio,
- shouldResizeFromCenter,
- },
- );
- resizeSingleElement(
- nextWidth,
- nextHeight,
- latestElement,
- origElement,
- elementsMap,
- originalElements,
- transformHandleType,
- {
- shouldMaintainAspectRatio,
- shouldResizeFromCenter,
- },
- );
- }
- }
- return true;
- } else if (selectedElements.length > 1) {
- if (transformHandleType === "rotation") {
- rotateMultipleElements(
- originalElements,
- selectedElements,
- elementsMap,
- scene,
- pointerX,
- pointerY,
- shouldRotateWithDiscreteAngle,
- centerX,
- centerY,
- );
- return true;
- } else if (transformHandleType) {
- const { nextWidth, nextHeight, flipByX, flipByY, originalBoundingBox } =
- getNextMultipleWidthAndHeightFromPointer(
- selectedElements,
- originalElements,
- elementsMap,
- transformHandleType,
- pointerX,
- pointerY,
- {
- shouldMaintainAspectRatio,
- shouldResizeFromCenter,
- },
- );
- resizeMultipleElements(
- selectedElements,
- elementsMap,
- transformHandleType,
- scene,
- originalElements,
- {
- shouldResizeFromCenter,
- shouldMaintainAspectRatio,
- flipByX,
- flipByY,
- nextWidth,
- nextHeight,
- originalBoundingBox,
- },
- );
- return true;
- }
- }
- return false;
- };
- const rotateSingleElement = (
- element: NonDeletedExcalidrawElement,
- elementsMap: ElementsMap,
- scene: Scene,
- pointerX: number,
- pointerY: number,
- shouldRotateWithDiscreteAngle: boolean,
- ) => {
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
- const cx = (x1 + x2) / 2;
- const cy = (y1 + y2) / 2;
- let angle: Radians;
- if (isFrameLikeElement(element)) {
- angle = 0 as Radians;
- } else {
- angle = ((5 * Math.PI) / 2 +
- Math.atan2(pointerY - cy, pointerX - cx)) as Radians;
- if (shouldRotateWithDiscreteAngle) {
- angle = (angle + SHIFT_LOCKING_ANGLE / 2) as Radians;
- angle = (angle - (angle % SHIFT_LOCKING_ANGLE)) as Radians;
- }
- angle = normalizeRadians(angle as Radians);
- }
- const boundTextElementId = getBoundTextElementId(element);
- mutateElement(element, { angle });
- if (boundTextElementId) {
- const textElement =
- scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
- if (textElement && !isArrowElement(element)) {
- mutateElement(textElement, { angle });
- }
- }
- };
- export const rescalePointsInElement = (
- element: NonDeletedExcalidrawElement,
- width: number,
- height: number,
- normalizePoints: boolean,
- ) =>
- isLinearElement(element) || isFreeDrawElement(element)
- ? {
- points: rescalePoints(
- 0,
- width,
- rescalePoints(1, height, element.points, normalizePoints),
- normalizePoints,
- ),
- }
- : {};
- export const measureFontSizeFromWidth = (
- element: NonDeleted<ExcalidrawTextElement>,
- elementsMap: ElementsMap,
- nextWidth: number,
- ): { size: number } | null => {
- // We only use width to scale font on resize
- let width = element.width;
- const hasContainer = isBoundToContainer(element);
- if (hasContainer) {
- const container = getContainerElement(element, elementsMap);
- if (container) {
- width = getBoundTextMaxWidth(container, element);
- }
- }
- const nextFontSize = element.fontSize * (nextWidth / width);
- if (nextFontSize < MIN_FONT_SIZE) {
- return null;
- }
- return {
- size: nextFontSize,
- };
- };
- const resizeSingleTextElement = (
- originalElements: PointerDownState["originalElements"],
- element: NonDeleted<ExcalidrawTextElement>,
- elementsMap: ElementsMap,
- transformHandleType: TransformHandleDirection,
- shouldResizeFromCenter: boolean,
- pointerX: number,
- pointerY: number,
- ) => {
- 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 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 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,
- );
- const rotatedNewCenter = pointRotateRads(
- newCenter,
- pointFrom(cx, cy),
- angle,
- );
- newTopLeft = pointRotateRads(
- rotatedTopLeft,
- rotatedNewCenter,
- -angle as Radians,
- );
- const [nextX, nextY] = newTopLeft;
- mutateElement(element, {
- fontSize: metrics.size,
- width: nextWidth,
- height: nextHeight,
- x: nextX,
- y: nextY,
- });
- }
- 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,
- fontFamily: element.fontFamily,
- }),
- 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 text = wrapText(
- element.originalText,
- getFontString(element),
- Math.abs(newWidth),
- );
- const metrics = measureText(
- text,
- getFontString(element),
- element.lineHeight,
- );
- const eleNewHeight = 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],
- ];
- }
- // 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 resizedElement: Partial<ExcalidrawTextElement> = {
- width: Math.abs(newWidth),
- height: Math.abs(metrics.height),
- x: newTopLeft[0],
- y: newTopLeft[1],
- text,
- autoResize: false,
- };
- mutateElement(element, resizedElement);
- }
- };
- const rotateMultipleElements = (
- originalElements: PointerDownState["originalElements"],
- elements: readonly NonDeletedExcalidrawElement[],
- elementsMap: SceneElementsMap,
- scene: Scene,
- pointerX: number,
- pointerY: number,
- shouldRotateWithDiscreteAngle: boolean,
- centerX: number,
- centerY: number,
- ) => {
- let centerAngle =
- (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
- if (shouldRotateWithDiscreteAngle) {
- centerAngle += SHIFT_LOCKING_ANGLE / 2;
- centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
- }
- for (const element of elements) {
- if (!isFrameLikeElement(element)) {
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
- const cx = (x1 + x2) / 2;
- const cy = (y1 + y2) / 2;
- const origAngle =
- originalElements.get(element.id)?.angle ?? element.angle;
- const [rotatedCX, rotatedCY] = pointRotateRads(
- pointFrom(cx, cy),
- pointFrom(centerX, centerY),
- (centerAngle + origAngle - element.angle) as Radians,
- );
- if (isElbowArrow(element)) {
- // Needed to re-route the arrow
- mutateElement(element, {
- points: getArrowLocalFixedPoints(element, elementsMap),
- });
- } else {
- mutateElement(
- element,
- {
- x: element.x + (rotatedCX - cx),
- y: element.y + (rotatedCY - cy),
- angle: normalizeRadians((centerAngle + origAngle) as Radians),
- },
- false,
- );
- }
- updateBoundElements(element, elementsMap, {
- simultaneouslyUpdated: elements,
- });
- const boundText = getBoundTextElement(element, elementsMap);
- if (boundText && !isArrowElement(element)) {
- mutateElement(
- boundText,
- {
- x: boundText.x + (rotatedCX - cx),
- y: boundText.y + (rotatedCY - cy),
- angle: normalizeRadians((centerAngle + origAngle) as Radians),
- },
- false,
- );
- }
- }
- }
- scene.triggerUpdate();
- };
- export const getResizeOffsetXY = (
- transformHandleType: MaybeTransformHandleType,
- selectedElements: NonDeletedExcalidrawElement[],
- elementsMap: ElementsMap,
- x: number,
- y: number,
- ): [number, number] => {
- const [x1, y1, x2, y2] =
- selectedElements.length === 1
- ? getElementAbsoluteCoords(selectedElements[0], elementsMap)
- : getCommonBounds(selectedElements);
- const cx = (x1 + x2) / 2;
- const cy = (y1 + y2) / 2;
- const angle = (
- selectedElements.length === 1 ? selectedElements[0].angle : 0
- ) as Radians;
- [x, y] = pointRotateRads(
- pointFrom(x, y),
- pointFrom(cx, cy),
- -angle as Radians,
- );
- switch (transformHandleType) {
- case "n":
- return pointRotateRads(
- pointFrom(x - (x1 + x2) / 2, y - y1),
- pointFrom(0, 0),
- angle,
- );
- case "s":
- return pointRotateRads(
- pointFrom(x - (x1 + x2) / 2, y - y2),
- pointFrom(0, 0),
- angle,
- );
- case "w":
- return pointRotateRads(
- pointFrom(x - x1, y - (y1 + y2) / 2),
- pointFrom(0, 0),
- angle,
- );
- case "e":
- return pointRotateRads(
- pointFrom(x - x2, y - (y1 + y2) / 2),
- pointFrom(0, 0),
- angle,
- );
- case "nw":
- return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle);
- case "ne":
- return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
- case "sw":
- return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle);
- case "se":
- return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
- default:
- return [0, 0];
- }
- };
- export const getResizeArrowDirection = (
- transformHandleType: MaybeTransformHandleType,
- element: NonDeleted<ExcalidrawLinearElement>,
- ): "origin" | "end" => {
- const [, [px, py]] = element.points;
- const isResizeEnd =
- (transformHandleType === "nw" && (px < 0 || py < 0)) ||
- (transformHandleType === "ne" && px >= 0) ||
- (transformHandleType === "sw" && px <= 0) ||
- (transformHandleType === "se" && (px > 0 || py > 0));
- return isResizeEnd ? "end" : "origin";
- };
- type ResizeAnchor =
- | "top-left"
- | "top-right"
- | "bottom-left"
- | "bottom-right"
- | "west-side"
- | "north-side"
- | "east-side"
- | "south-side"
- | "center";
- const getResizeAnchor = (
- handleDirection: TransformHandleDirection,
- shouldMaintainAspectRatio: boolean,
- shouldResizeFromCenter: boolean,
- ): ResizeAnchor => {
- if (shouldResizeFromCenter) {
- return "center";
- }
- if (shouldMaintainAspectRatio) {
- switch (handleDirection) {
- case "n":
- return "south-side";
- case "e": {
- return "west-side";
- }
- case "s":
- return "north-side";
- case "w":
- return "east-side";
- case "ne":
- return "bottom-left";
- case "nw":
- return "bottom-right";
- case "se":
- return "top-left";
- case "sw":
- return "top-right";
- }
- }
- if (["e", "se", "s"].includes(handleDirection)) {
- return "top-left";
- } else if (["n", "nw", "w"].includes(handleDirection)) {
- return "bottom-right";
- } else if (handleDirection === "ne") {
- return "bottom-left";
- }
- return "top-right";
- };
- const getResizedOrigin = (
- prevOrigin: GlobalPoint,
- prevWidth: number,
- prevHeight: number,
- newWidth: number,
- newHeight: number,
- angle: number,
- handleDirection: TransformHandleDirection,
- shouldMaintainAspectRatio: boolean,
- shouldResizeFromCenter: boolean,
- ): { x: number; y: number } => {
- const anchor = getResizeAnchor(
- handleDirection,
- shouldMaintainAspectRatio,
- shouldResizeFromCenter,
- );
- const [x, y] = prevOrigin;
- switch (anchor) {
- case "top-left":
- return {
- x:
- x +
- (prevWidth - newWidth) / 2 +
- ((newWidth - prevWidth) / 2) * Math.cos(angle) +
- ((prevHeight - newHeight) / 2) * Math.sin(angle),
- y:
- y +
- (prevHeight - newHeight) / 2 +
- ((newWidth - prevWidth) / 2) * Math.sin(angle) +
- ((newHeight - prevHeight) / 2) * Math.cos(angle),
- };
- case "top-right":
- return {
- x:
- x +
- ((prevWidth - newWidth) / 2) * (Math.cos(angle) + 1) +
- ((prevHeight - newHeight) / 2) * Math.sin(angle),
- y:
- y +
- (prevHeight - newHeight) / 2 +
- ((prevWidth - newWidth) / 2) * Math.sin(angle) +
- ((newHeight - prevHeight) / 2) * Math.cos(angle),
- };
- case "bottom-left":
- return {
- x:
- x +
- ((prevWidth - newWidth) / 2) * (1 - Math.cos(angle)) +
- ((newHeight - prevHeight) / 2) * Math.sin(angle),
- y:
- y +
- ((prevHeight - newHeight) / 2) * (Math.cos(angle) + 1) +
- ((newWidth - prevWidth) / 2) * Math.sin(angle),
- };
- case "bottom-right":
- return {
- x:
- x +
- ((prevWidth - newWidth) / 2) * (Math.cos(angle) + 1) +
- ((newHeight - prevHeight) / 2) * Math.sin(angle),
- y:
- y +
- ((prevHeight - newHeight) / 2) * (Math.cos(angle) + 1) +
- ((prevWidth - newWidth) / 2) * Math.sin(angle),
- };
- case "center":
- return {
- x: x - (newWidth - prevWidth) / 2,
- y: y - (newHeight - prevHeight) / 2,
- };
- case "east-side":
- return {
- x: x + ((prevWidth - newWidth) / 2) * (Math.cos(angle) + 1),
- y:
- y +
- ((prevWidth - newWidth) / 2) * Math.sin(angle) +
- (prevHeight - newHeight) / 2,
- };
- case "west-side":
- return {
- x: x + ((prevWidth - newWidth) / 2) * (1 - Math.cos(angle)),
- y:
- y +
- ((newWidth - prevWidth) / 2) * Math.sin(angle) +
- (prevHeight - newHeight) / 2,
- };
- case "north-side":
- return {
- x:
- x +
- (prevWidth - newWidth) / 2 +
- ((prevHeight - newHeight) / 2) * Math.sin(angle),
- y: y + ((newHeight - prevHeight) / 2) * (Math.cos(angle) - 1),
- };
- case "south-side":
- return {
- x:
- x +
- (prevWidth - newWidth) / 2 +
- ((newHeight - prevHeight) / 2) * Math.sin(angle),
- y: y + ((prevHeight - newHeight) / 2) * (Math.cos(angle) + 1),
- };
- }
- };
- export const resizeSingleElement = (
- nextWidth: number,
- nextHeight: number,
- latestElement: ExcalidrawElement,
- origElement: ExcalidrawElement,
- elementsMap: ElementsMap,
- originalElementsMap: ElementsMap,
- handleDirection: TransformHandleDirection,
- {
- shouldInformMutation = true,
- shouldMaintainAspectRatio = false,
- shouldResizeFromCenter = false,
- }: {
- shouldMaintainAspectRatio?: boolean;
- shouldResizeFromCenter?: boolean;
- shouldInformMutation?: boolean;
- } = {},
- ) => {
- let boundTextFont: { fontSize?: number } = {};
- const boundTextElement = getBoundTextElement(latestElement, elementsMap);
- if (boundTextElement) {
- const stateOfBoundTextElementAtResize = originalElementsMap.get(
- boundTextElement.id,
- ) as typeof boundTextElement | undefined;
- if (stateOfBoundTextElementAtResize) {
- boundTextFont = {
- fontSize: stateOfBoundTextElementAtResize.fontSize,
- };
- }
- if (shouldMaintainAspectRatio) {
- const updatedElement = {
- ...latestElement,
- width: nextWidth,
- height: nextHeight,
- };
- const nextFont = measureFontSizeFromWidth(
- boundTextElement,
- elementsMap,
- getBoundTextMaxWidth(updatedElement, boundTextElement),
- );
- if (nextFont === null) {
- return;
- }
- boundTextFont = {
- fontSize: nextFont.size,
- };
- } 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);
- }
- }
- const rescaledPoints = rescalePointsInElement(
- origElement,
- nextWidth,
- nextHeight,
- true,
- );
- let previousOrigin = pointFrom<GlobalPoint>(origElement.x, origElement.y);
- if (isLinearElement(origElement)) {
- const [x1, y1] = getElementBounds(origElement, originalElementsMap);
- previousOrigin = pointFrom<GlobalPoint>(x1, y1);
- }
- const newOrigin: {
- x: number;
- y: number;
- } = getResizedOrigin(
- previousOrigin,
- origElement.width,
- origElement.height,
- nextWidth,
- nextHeight,
- origElement.angle,
- handleDirection,
- shouldMaintainAspectRatio!!,
- shouldResizeFromCenter!!,
- );
- if (isLinearElement(origElement) && rescaledPoints.points) {
- const offsetX = origElement.x - previousOrigin[0];
- const offsetY = origElement.y - previousOrigin[1];
- newOrigin.x += offsetX;
- newOrigin.y += offsetY;
- const scaledX = rescaledPoints.points[0][0];
- const scaledY = rescaledPoints.points[0][1];
- newOrigin.x += scaledX;
- newOrigin.y += scaledY;
- rescaledPoints.points = rescaledPoints.points.map((p) =>
- pointFrom<LocalPoint>(p[0] - scaledX, p[1] - scaledY),
- );
- }
- // flipping
- if (nextWidth < 0) {
- newOrigin.x = newOrigin.x + nextWidth;
- }
- if (nextHeight < 0) {
- newOrigin.y = newOrigin.y + nextHeight;
- }
- if ("scale" in latestElement && "scale" in origElement) {
- mutateElement(latestElement, {
- scale: [
- // defaulting because scaleX/Y can be 0/-0
- (Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
- (Math.sign(nextHeight) || origElement.scale[1]) * origElement.scale[1],
- ],
- });
- }
- if (
- isArrowElement(latestElement) &&
- boundTextElement &&
- shouldMaintainAspectRatio
- ) {
- const fontSize =
- (nextWidth / latestElement.width) * boundTextElement.fontSize;
- if (fontSize < MIN_FONT_SIZE) {
- return;
- }
- boundTextFont.fontSize = fontSize;
- }
- if (
- nextWidth !== 0 &&
- nextHeight !== 0 &&
- Number.isFinite(newOrigin.x) &&
- Number.isFinite(newOrigin.y)
- ) {
- const updates = {
- ...newOrigin,
- width: Math.abs(nextWidth),
- height: Math.abs(nextHeight),
- ...rescaledPoints,
- };
- mutateElement(latestElement, updates, shouldInformMutation);
- updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
- // TODO: confirm with MARK if this actually makes sense
- newSize: { width: nextWidth, height: nextHeight },
- });
- if (boundTextElement && boundTextFont != null) {
- mutateElement(boundTextElement, {
- fontSize: boundTextFont.fontSize,
- });
- }
- handleBindTextResize(
- latestElement,
- elementsMap,
- handleDirection,
- shouldMaintainAspectRatio,
- );
- }
- };
- const getNextSingleWidthAndHeightFromPointer = (
- latestElement: ExcalidrawElement,
- origElement: ExcalidrawElement,
- elementsMap: ElementsMap,
- originalElementsMap: ElementsMap,
- handleDirection: TransformHandleDirection,
- pointerX: number,
- pointerY: number,
- {
- shouldMaintainAspectRatio = false,
- shouldResizeFromCenter = false,
- }: {
- shouldMaintainAspectRatio?: boolean;
- shouldResizeFromCenter?: boolean;
- } = {},
- ) => {
- // Gets bounds corners
- const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
- origElement,
- origElement.width,
- origElement.height,
- true,
- );
- const startTopLeft = pointFrom(x1, y1);
- const startBottomRight = pointFrom(x2, y2);
- const startCenter = pointCenter(startTopLeft, startBottomRight);
- // Calculate new dimensions based on cursor position
- const rotatedPointer = pointRotateRads(
- pointFrom(pointerX, pointerY),
- startCenter,
- -origElement.angle as Radians,
- );
- // Get bounds corners rendered on screen
- const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
- latestElement,
- latestElement.width,
- latestElement.height,
- true,
- );
- const boundsCurrentWidth = esx2 - esx1;
- const boundsCurrentHeight = esy2 - esy1;
- // It's important we set the initial scale value based on the width and height at resize start,
- // otherwise previous dimensions affected by modifiers will be taken into account.
- const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
- const atStartBoundsHeight = startBottomRight[1] - startTopLeft[1];
- let scaleX = atStartBoundsWidth / boundsCurrentWidth;
- let scaleY = atStartBoundsHeight / boundsCurrentHeight;
- if (handleDirection.includes("e")) {
- scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
- }
- if (handleDirection.includes("s")) {
- scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
- }
- if (handleDirection.includes("w")) {
- scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
- }
- if (handleDirection.includes("n")) {
- scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
- }
- // We have to use dimensions of element on screen, otherwise the scaling of the
- // dimensions won't match the cursor for linear elements.
- let nextWidth = latestElement.width * scaleX;
- let nextHeight = latestElement.height * scaleY;
- if (shouldResizeFromCenter) {
- nextWidth = 2 * nextWidth - origElement.width;
- nextHeight = 2 * nextHeight - origElement.height;
- }
- // adjust dimensions to keep sides ratio
- if (shouldMaintainAspectRatio) {
- const widthRatio = Math.abs(nextWidth) / origElement.width;
- const heightRatio = Math.abs(nextHeight) / origElement.height;
- if (handleDirection.length === 1) {
- nextHeight *= widthRatio;
- nextWidth *= heightRatio;
- }
- if (handleDirection.length === 2) {
- const ratio = Math.max(widthRatio, heightRatio);
- nextWidth = origElement.width * ratio * Math.sign(nextWidth);
- nextHeight = origElement.height * ratio * Math.sign(nextHeight);
- }
- }
- return {
- nextWidth,
- nextHeight,
- };
- };
- const getNextMultipleWidthAndHeightFromPointer = (
- selectedElements: readonly NonDeletedExcalidrawElement[],
- originalElementsMap: ElementsMap,
- elementsMap: ElementsMap,
- handleDirection: TransformHandleDirection,
- pointerX: number,
- pointerY: number,
- {
- shouldMaintainAspectRatio = false,
- shouldResizeFromCenter = false,
- }: {
- shouldResizeFromCenter?: boolean;
- shouldMaintainAspectRatio?: boolean;
- } = {},
- ) => {
- const originalElementsArray = selectedElements.map(
- (el) => originalElementsMap.get(el.id)!,
- );
- // getCommonBoundingBox() uses getBoundTextElement() which returns null for
- // original elements from pointerDownState, so we have to find and add these
- // bound text elements manually. Additionally, the coordinates of bound text
- // elements aren't always up to date.
- const boundTextElements = originalElementsArray.reduce((acc, orig) => {
- if (!isLinearElement(orig)) {
- return acc;
- }
- const textId = getBoundTextElementId(orig);
- if (!textId) {
- return acc;
- }
- const text = originalElementsMap.get(textId) ?? null;
- if (!isBoundToContainer(text)) {
- return acc;
- }
- return [
- ...acc,
- {
- ...text,
- ...LinearElementEditor.getBoundTextElementPosition(
- orig,
- text,
- elementsMap,
- ),
- },
- ];
- }, [] as ExcalidrawTextElementWithContainer[]);
- const originalBoundingBox = getCommonBoundingBox(
- originalElementsArray.map((orig) => orig).concat(boundTextElements),
- );
- const { minX, minY, maxX, maxY, midX, midY } = originalBoundingBox;
- const width = maxX - minX;
- const height = maxY - minY;
- const anchorsMap = {
- ne: [minX, maxY],
- se: [minX, minY],
- sw: [maxX, minY],
- nw: [maxX, maxY],
- e: [minX, minY + height / 2],
- w: [maxX, minY + height / 2],
- n: [minX + width / 2, maxY],
- s: [minX + width / 2, minY],
- } as Record<TransformHandleDirection, GlobalPoint>;
- // anchor point must be on the opposite side of the dragged selection handle
- // or be the center of the selection if shouldResizeFromCenter
- const [anchorX, anchorY] = shouldResizeFromCenter
- ? [midX, midY]
- : anchorsMap[handleDirection];
- const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
- const scale =
- Math.max(
- Math.abs(pointerX - anchorX) / width || 0,
- Math.abs(pointerY - anchorY) / height || 0,
- ) * resizeFromCenterScale;
- let nextWidth =
- handleDirection.includes("e") || handleDirection.includes("w")
- ? Math.abs(pointerX - anchorX) * resizeFromCenterScale
- : width;
- let nextHeight =
- handleDirection.includes("n") || handleDirection.includes("s")
- ? Math.abs(pointerY - anchorY) * resizeFromCenterScale
- : height;
- if (shouldMaintainAspectRatio) {
- nextWidth = width * scale * Math.sign(pointerX - anchorX);
- nextHeight = height * scale * Math.sign(pointerY - anchorY);
- }
- const flipConditionsMap: Record<
- TransformHandleDirection,
- // Condition for which we should flip or not flip the selected elements
- // - when evaluated to `true`, we flip
- // - therefore, setting it to always `false` means we do not flip (in that direction) at all
- [x: boolean, y: boolean]
- > = {
- ne: [pointerX < anchorX, pointerY > anchorY],
- se: [pointerX < anchorX, pointerY < anchorY],
- sw: [pointerX > anchorX, pointerY < anchorY],
- nw: [pointerX > anchorX, pointerY > anchorY],
- // e.g. when resizing from the "e" side, we do not need to consider changes in the `y` direction
- // and therefore, we do not need to flip in the `y` direction at all
- e: [pointerX < anchorX, false],
- w: [pointerX > anchorX, false],
- n: [false, pointerY > anchorY],
- s: [false, pointerY < anchorY],
- };
- const [flipByX, flipByY] = flipConditionsMap[handleDirection].map(
- (condition) => condition,
- );
- return {
- originalBoundingBox,
- nextWidth,
- nextHeight,
- flipByX,
- flipByY,
- };
- };
- export const resizeMultipleElements = (
- selectedElements: readonly NonDeletedExcalidrawElement[],
- elementsMap: ElementsMap,
- handleDirection: TransformHandleDirection,
- scene: Scene,
- originalElementsMap: ElementsMap,
- {
- shouldMaintainAspectRatio = false,
- shouldResizeFromCenter = false,
- flipByX = false,
- flipByY = false,
- nextHeight,
- nextWidth,
- originalBoundingBox,
- }: {
- nextWidth?: number;
- nextHeight?: number;
- shouldMaintainAspectRatio?: boolean;
- shouldResizeFromCenter?: boolean;
- flipByX?: boolean;
- flipByY?: boolean;
- // added to improve performance
- originalBoundingBox?: BoundingBox;
- } = {},
- ) => {
- // in the case of just flipping, there is no need to specify the next width and height
- if (
- nextWidth === undefined &&
- nextHeight === undefined &&
- flipByX === undefined &&
- flipByY === undefined
- ) {
- return;
- }
- // do not allow next width or height to be 0
- if (nextHeight === 0 || nextWidth === 0) {
- return;
- }
- if (!originalElementsMap) {
- originalElementsMap = elementsMap;
- }
- const targetElements = selectedElements.reduce(
- (
- acc: {
- /** element at resize start */
- orig: NonDeletedExcalidrawElement;
- /** latest element */
- latest: NonDeletedExcalidrawElement;
- }[],
- element,
- ) => {
- const origElement = originalElementsMap!.get(element.id);
- if (origElement) {
- acc.push({ orig: origElement, latest: element });
- }
- return acc;
- },
- [],
- );
- let boundingBox: BoundingBox;
- if (originalBoundingBox) {
- boundingBox = originalBoundingBox;
- } else {
- const boundTextElements = targetElements.reduce((acc, { orig }) => {
- if (!isLinearElement(orig)) {
- return acc;
- }
- const textId = getBoundTextElementId(orig);
- if (!textId) {
- return acc;
- }
- const text = originalElementsMap!.get(textId) ?? null;
- if (!isBoundToContainer(text)) {
- return acc;
- }
- return [
- ...acc,
- {
- ...text,
- ...LinearElementEditor.getBoundTextElementPosition(
- orig,
- text,
- elementsMap,
- ),
- },
- ];
- }, [] as ExcalidrawTextElementWithContainer[]);
- boundingBox = getCommonBoundingBox(
- targetElements.map(({ orig }) => orig).concat(boundTextElements),
- );
- }
- const { minX, minY, maxX, maxY, midX, midY } = boundingBox;
- const width = maxX - minX;
- const height = maxY - minY;
- if (nextWidth === undefined && nextHeight === undefined) {
- nextWidth = width;
- nextHeight = height;
- }
- if (shouldMaintainAspectRatio) {
- if (nextWidth === undefined) {
- nextWidth = nextHeight! * (width / height);
- } else if (nextHeight === undefined) {
- nextHeight = nextWidth! * (height / width);
- } else if (Math.abs(nextWidth / nextHeight - width / height) > 0.001) {
- nextWidth = nextHeight * (width / height);
- }
- }
- if (nextWidth && nextHeight) {
- let scaleX =
- handleDirection.includes("e") || handleDirection.includes("w")
- ? Math.abs(nextWidth) / width
- : 1;
- let scaleY =
- handleDirection.includes("n") || handleDirection.includes("s")
- ? Math.abs(nextHeight) / height
- : 1;
- let scale: number;
- if (handleDirection.length === 1) {
- scale =
- handleDirection.includes("e") || handleDirection.includes("w")
- ? scaleX
- : scaleY;
- } else {
- scale = Math.max(
- Math.abs(nextWidth) / width || 0,
- Math.abs(nextHeight) / height || 0,
- );
- }
- const anchorsMap = {
- ne: [minX, maxY],
- se: [minX, minY],
- sw: [maxX, minY],
- nw: [maxX, maxY],
- e: [minX, minY + height / 2],
- w: [maxX, minY + height / 2],
- n: [minX + width / 2, maxY],
- s: [minX + width / 2, minY],
- } as Record<TransformHandleDirection, GlobalPoint>;
- // anchor point must be on the opposite side of the dragged selection handle
- // or be the center of the selection if shouldResizeFromCenter
- const [anchorX, anchorY] = shouldResizeFromCenter
- ? [midX, midY]
- : anchorsMap[handleDirection];
- const keepAspectRatio =
- shouldMaintainAspectRatio ||
- targetElements.some(
- (item) =>
- item.latest.angle !== 0 ||
- isTextElement(item.latest) ||
- isInGroup(item.latest),
- );
- if (keepAspectRatio) {
- scaleX = scale;
- scaleY = scale;
- }
- /**
- * to flip an element:
- * 1. determine over which axis is the element being flipped
- * (could be x, y, or both) indicated by `flipFactorX` & `flipFactorY`
- * 2. shift element's position by the amount of width or height (or both) or
- * mirror points in the case of linear & freedraw elemenets
- * 3. adjust element angle
- */
- const [flipFactorX, flipFactorY] = [flipByX ? -1 : 1, flipByY ? -1 : 1];
- const elementsAndUpdates: {
- element: NonDeletedExcalidrawElement;
- update: Mutable<
- Pick<ExcalidrawElement, "x" | "y" | "width" | "height" | "angle">
- > & {
- points?: ExcalidrawLinearElement["points"];
- fontSize?: ExcalidrawTextElement["fontSize"];
- scale?: ExcalidrawImageElement["scale"];
- boundTextFontSize?: ExcalidrawTextElement["fontSize"];
- startBinding?: ExcalidrawElbowArrowElement["startBinding"];
- endBinding?: ExcalidrawElbowArrowElement["endBinding"];
- fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"];
- };
- }[] = [];
- for (const { orig, latest } of targetElements) {
- // bounded text elements are updated along with their container elements
- if (isTextElement(orig) && isBoundToContainer(orig)) {
- continue;
- }
- const width = orig.width * scaleX;
- const height = orig.height * scaleY;
- const angle = normalizeRadians(
- (orig.angle * flipFactorX * flipFactorY) as Radians,
- );
- const isLinearOrFreeDraw =
- isLinearElement(orig) || isFreeDrawElement(orig);
- const offsetX = orig.x - anchorX;
- const offsetY = orig.y - anchorY;
- const shiftX = flipByX && !isLinearOrFreeDraw ? width : 0;
- const shiftY = flipByY && !isLinearOrFreeDraw ? height : 0;
- const x = anchorX + flipFactorX * (offsetX * scaleX + shiftX);
- const y = anchorY + flipFactorY * (offsetY * scaleY + shiftY);
- const rescaledPoints = rescalePointsInElement(
- orig,
- width * flipFactorX,
- height * flipFactorY,
- false,
- );
- const update: typeof elementsAndUpdates[0]["update"] = {
- x,
- y,
- width,
- height,
- angle,
- ...rescaledPoints,
- };
- if (isElbowArrow(orig)) {
- // Mirror fixed point binding for elbow arrows
- // when resize goes into the negative direction
- if (orig.startBinding) {
- update.startBinding = {
- ...orig.startBinding,
- fixedPoint: [
- flipByX
- ? -orig.startBinding.fixedPoint[0] + 1
- : orig.startBinding.fixedPoint[0],
- flipByY
- ? -orig.startBinding.fixedPoint[1] + 1
- : orig.startBinding.fixedPoint[1],
- ],
- };
- }
- if (orig.endBinding) {
- update.endBinding = {
- ...orig.endBinding,
- fixedPoint: [
- flipByX
- ? -orig.endBinding.fixedPoint[0] + 1
- : orig.endBinding.fixedPoint[0],
- flipByY
- ? -orig.endBinding.fixedPoint[1] + 1
- : orig.endBinding.fixedPoint[1],
- ],
- };
- }
- if (orig.fixedSegments && rescaledPoints.points) {
- update.fixedSegments = orig.fixedSegments.map((segment) => ({
- ...segment,
- start: rescaledPoints.points[segment.index - 1],
- end: rescaledPoints.points[segment.index],
- }));
- }
- }
- if (isImageElement(orig)) {
- update.scale = [
- orig.scale[0] * flipFactorX,
- orig.scale[1] * flipFactorY,
- ];
- }
- if (isTextElement(orig)) {
- const metrics = measureFontSizeFromWidth(orig, elementsMap, width);
- if (!metrics) {
- return;
- }
- update.fontSize = metrics.size;
- }
- const boundTextElement = originalElementsMap.get(
- getBoundTextElementId(orig) ?? "",
- ) as ExcalidrawTextElementWithContainer | undefined;
- if (boundTextElement) {
- if (keepAspectRatio) {
- const newFontSize = boundTextElement.fontSize * scale;
- if (newFontSize < MIN_FONT_SIZE) {
- return;
- }
- update.boundTextFontSize = newFontSize;
- } else {
- update.boundTextFontSize = boundTextElement.fontSize;
- }
- }
- elementsAndUpdates.push({
- element: latest,
- update,
- });
- }
- const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
- for (const {
- element,
- update: { boundTextFontSize, ...update },
- } of elementsAndUpdates) {
- const { width, height, angle } = update;
- mutateElement(element, update, false, {
- // needed for the fixed binding point udpate to take effect
- isDragging: true,
- });
- updateBoundElements(element, elementsMap as SceneElementsMap, {
- simultaneouslyUpdated: elementsToUpdate,
- newSize: { width, height },
- });
- const boundTextElement = getBoundTextElement(element, elementsMap);
- if (boundTextElement && boundTextFontSize) {
- mutateElement(
- boundTextElement,
- {
- fontSize: boundTextFontSize,
- angle: isLinearElement(element) ? undefined : angle,
- },
- false,
- );
- handleBindTextResize(element, elementsMap, handleDirection, true);
- }
- }
- scene.triggerUpdate();
- }
- };
|