From c68c2be44c29442ce8ddec365ef4a156dbeb36a4 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Tue, 4 Jun 2024 23:06:27 +0800 Subject: [PATCH] handle bound texts --- .../excalidraw/components/Stats/Angle.tsx | 35 ++++-- .../excalidraw/components/Stats/Dimension.tsx | 107 +++++++++++++++--- .../excalidraw/components/Stats/DragInput.tsx | 66 +++++++---- .../excalidraw/components/Stats/FontSize.tsx | 6 +- .../components/Stats/MultiDimension.tsx | 100 +++++++++++++--- .../excalidraw/components/Stats/index.tsx | 27 ++++- packages/excalidraw/element/resizeElements.ts | 2 +- 7 files changed, 271 insertions(+), 72 deletions(-) diff --git a/packages/excalidraw/components/Stats/Angle.tsx b/packages/excalidraw/components/Stats/Angle.tsx index c0c303c7ff..6d8afc719b 100644 --- a/packages/excalidraw/components/Stats/Angle.tsx +++ b/packages/excalidraw/components/Stats/Angle.tsx @@ -1,5 +1,7 @@ import { mutateElement } from "../../element/mutateElement"; -import type { ExcalidrawElement } from "../../element/types"; +import { getBoundTextElement } from "../../element/textElement"; +import { isArrowElement } from "../../element/typeChecks"; +import type { ElementsMap, ExcalidrawElement } from "../../element/types"; import { degreeToRadian, radianToDegree } from "../../math"; import DragInput from "./DragInput"; import type { DragInputCallbackType } from "./DragInput"; @@ -7,19 +9,18 @@ import { getStepSizedValue, isPropertyEditable } from "./utils"; interface AngleProps { element: ExcalidrawElement; + elementsMap: ElementsMap; } const STEP_SIZE = 15; -const Angle = ({ element }: AngleProps) => { - const handleDegreeChange: DragInputCallbackType = ( +const Angle = ({ element, elementsMap }: AngleProps) => { + const handleDegreeChange: DragInputCallbackType = ({ accumulatedChange, - instantChange, stateAtStart, - shouldKeepAspectRatio, shouldChangeByStepSize, nextValue, - ) => { + }) => { const _stateAtStart = stateAtStart[0]; if (_stateAtStart) { if (nextValue !== undefined) { @@ -27,6 +28,12 @@ const Angle = ({ element }: AngleProps) => { mutateElement(element, { angle: nextAngle, }); + + const boundTextElement = getBoundTextElement(element, elementsMap); + if (boundTextElement && !isArrowElement(element)) { + mutateElement(boundTextElement, { angle: nextAngle }); + } + return; } @@ -38,13 +45,19 @@ const Angle = ({ element }: AngleProps) => { nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE); } + nextAngleInDegrees = + nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees; + + const nextAngle = degreeToRadian(nextAngleInDegrees); + mutateElement(element, { - angle: degreeToRadian( - nextAngleInDegrees < 0 - ? nextAngleInDegrees + 360 - : nextAngleInDegrees, - ), + angle: nextAngle, }); + + const boundTextElement = getBoundTextElement(element, elementsMap); + if (boundTextElement && !isArrowElement(element)) { + mutateElement(boundTextElement, { angle: nextAngle }); + } } }; diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx index 3d4725e964..87990b0aa2 100644 --- a/packages/excalidraw/components/Stats/Dimension.tsx +++ b/packages/excalidraw/components/Stats/Dimension.tsx @@ -1,13 +1,26 @@ -import type { ExcalidrawElement } from "../../element/types"; +import type { ElementsMap, ExcalidrawElement } from "../../element/types"; import DragInput from "./DragInput"; import type { DragInputCallbackType } from "./DragInput"; import { getStepSizedValue, isPropertyEditable } from "./utils"; import { mutateElement } from "../../element/mutateElement"; -import { rescalePointsInElement } from "../../element/resizeElements"; +import { + measureFontSizeFromWidth, + rescalePointsInElement, +} from "../../element/resizeElements"; +import { + getApproxMinLineHeight, + getApproxMinLineWidth, + getBoundTextElement, + getBoundTextMaxWidth, + handleBindTextResize, +} from "../../element/textElement"; +import { getFontString } from "../../utils"; +import { updateBoundElements } from "../../element/binding"; interface DimensionDragInputProps { property: "width" | "height"; element: ExcalidrawElement; + elementsMap: ElementsMap; } const STEP_SIZE = 10; @@ -51,13 +64,16 @@ export const newOrigin = ( }; }; -const getResizedUpdates = ( +const resizeElement = ( nextWidth: number, nextHeight: number, + keepAspectRatio: boolean, latestState: ExcalidrawElement, stateAtStart: ExcalidrawElement, + elementsMap: ElementsMap, + originalElementsMap: Map, ) => { - return { + mutateElement(latestState, { ...newOrigin( latestState.x, latestState.y, @@ -70,18 +86,72 @@ const getResizedUpdates = ( width: nextWidth, height: nextHeight, ...rescalePointsInElement(stateAtStart, nextWidth, nextHeight, true), - }; + }); + + let boundTextFont: { fontSize?: number } = {}; + const boundTextElement = getBoundTextElement(latestState, elementsMap); + + if (boundTextElement) { + boundTextFont = { + fontSize: boundTextElement.fontSize, + }; + if (keepAspectRatio) { + const updatedElement = { + ...latestState, + width: nextWidth, + height: nextHeight, + }; + + const nextFont = measureFontSizeFromWidth( + boundTextElement, + elementsMap, + getBoundTextMaxWidth(updatedElement, boundTextElement), + ); + boundTextFont = { + fontSize: nextFont?.size ?? boundTextElement.fontSize, + }; + } else { + const minWidth = getApproxMinLineWidth( + getFontString(boundTextElement), + boundTextElement.lineHeight, + ); + const minHeight = getApproxMinLineHeight( + boundTextElement.fontSize, + boundTextElement.lineHeight, + ); + nextWidth = Math.max(nextWidth, minWidth); + nextHeight = Math.max(nextHeight, minHeight); + } + } + + updateBoundElements(latestState, elementsMap, { + newSize: { + width: nextWidth, + height: nextHeight, + }, + }); + + if (boundTextElement && boundTextFont) { + mutateElement(boundTextElement, { + fontSize: boundTextFont.fontSize, + }); + } + handleBindTextResize(latestState, elementsMap, "e", keepAspectRatio); }; -const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { - const handleDimensionChange: DragInputCallbackType = ( +const DimensionDragInput = ({ + property, + element, + elementsMap, +}: DimensionDragInputProps) => { + const handleDimensionChange: DragInputCallbackType = ({ accumulatedChange, - instantChange, stateAtStart, + originalElementsMap, shouldKeepAspectRatio, shouldChangeByStepSize, nextValue, - ) => { + }) => { const _stateAtStart = stateAtStart[0]; if (_stateAtStart) { const keepAspectRatio = @@ -106,10 +176,16 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { 0, ); - mutateElement( + resizeElement( + nextWidth, + nextHeight, + keepAspectRatio, element, - getResizedUpdates(nextWidth, nextHeight, element, _stateAtStart), + _stateAtStart, + elementsMap, + originalElementsMap, ); + return; } const changeInWidth = property === "width" ? accumulatedChange : 0; @@ -141,9 +217,14 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { } } - mutateElement( + resizeElement( + nextWidth, + nextHeight, + keepAspectRatio, element, - getResizedUpdates(nextWidth, nextHeight, element, _stateAtStart), + _stateAtStart, + elementsMap, + originalElementsMap, ); } }; diff --git a/packages/excalidraw/components/Stats/DragInput.tsx b/packages/excalidraw/components/Stats/DragInput.tsx index a56d6dbfed..664322dd3c 100644 --- a/packages/excalidraw/components/Stats/DragInput.tsx +++ b/packages/excalidraw/components/Stats/DragInput.tsx @@ -2,21 +2,30 @@ import { useEffect, useMemo, useRef, useState } from "react"; import throttle from "lodash.throttle"; import { EVENT } from "../../constants"; import { KEYS } from "../../keys"; -import type { ExcalidrawElement } from "../../element/types"; +import type { ElementsMap, ExcalidrawElement } from "../../element/types"; import { deepCopyElement } from "../../element/newElement"; import "./DragInput.scss"; import clsx from "clsx"; import { useApp } from "../App"; -export type DragInputCallbackType = ( - accumulatedChange: number, - instantChange: number, - stateAtStart: ExcalidrawElement[], - shouldKeepAspectRatio: boolean, - shouldChangeByStepSize: boolean, - nextValue?: number, -) => void; +export type DragInputCallbackType = ({ + accumulatedChange, + instantChange, + stateAtStart, + originalElementsMap, + shouldKeepAspectRatio, + shouldChangeByStepSize, + nextValue, +}: { + accumulatedChange: number; + instantChange: number; + stateAtStart: ExcalidrawElement[]; + originalElementsMap: ElementsMap; + shouldKeepAspectRatio: boolean; + shouldChangeByStepSize: boolean; + nextValue?: number; +}) => void; interface StatsDragInputProps { label: string | React.ReactNode; @@ -67,6 +76,8 @@ const StatsDragInput = ({ } | null = null; let stateAtStart: ExcalidrawElement[] | null = null; + let originalElementsMap: Map | null = + null; let accumulatedChange: number | null = null; @@ -79,6 +90,15 @@ const StatsDragInput = ({ ); } + if (!originalElementsMap) { + originalElementsMap = app.scene + .getNonDeletedElements() + .reduce((acc, element) => { + acc.set(element.id, deepCopyElement(element)); + return acc; + }, new Map() as ElementsMap); + } + if (!accumulatedChange) { accumulatedChange = 0; } @@ -87,13 +107,14 @@ const StatsDragInput = ({ const instantChange = event.clientX - lastPointer.x; accumulatedChange += instantChange; - cbThrottled( + cbThrottled({ accumulatedChange, instantChange, stateAtStart, - shouldKeepAspectRatio!!, - event.shiftKey, - ); + originalElementsMap, + shouldKeepAspectRatio: shouldKeepAspectRatio!!, + shouldChangeByStepSize: event.shiftKey, + }); } lastPointer = { @@ -117,6 +138,7 @@ const StatsDragInput = ({ lastPointer = null; accumulatedChange = null; stateAtStart = null; + originalElementsMap = null; document.body.classList.remove("dragResize"); }, @@ -149,14 +171,16 @@ const StatsDragInput = ({ setInputValue(value.toString()); return; } - dragInputCallback( - 0, - 0, - elements, - shouldKeepAspectRatio!!, - false, - v, - ); + + dragInputCallback({ + accumulatedChange: 0, + instantChange: 0, + stateAtStart: elements, + originalElementsMap: app.scene.getNonDeletedElementsMap(), + shouldKeepAspectRatio: shouldKeepAspectRatio!!, + shouldChangeByStepSize: false, + nextValue: v, + }); app.store.shouldCaptureIncrement(); eventTarget.blur(); } diff --git a/packages/excalidraw/components/Stats/FontSize.tsx b/packages/excalidraw/components/Stats/FontSize.tsx index 724fced230..68d54f91c7 100644 --- a/packages/excalidraw/components/Stats/FontSize.tsx +++ b/packages/excalidraw/components/Stats/FontSize.tsx @@ -14,14 +14,12 @@ const MIN_FONT_SIZE = 4; const STEP_SIZE = 4; const FontSize = ({ element, elementsMap }: FontSizeProps) => { - const handleFontSizeChange: DragInputCallbackType = ( + const handleFontSizeChange: DragInputCallbackType = ({ accumulatedChange, - instantChange, stateAtStart, - shouldKeepAspectRatio, shouldChangeByStepSize, nextValue, - ) => { + }) => { const _stateAtStart = stateAtStart[0]; if (_stateAtStart) { if (nextValue) { diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx index aed1d8c8be..29ffacfedd 100644 --- a/packages/excalidraw/components/Stats/MultiDimension.tsx +++ b/packages/excalidraw/components/Stats/MultiDimension.tsx @@ -1,7 +1,12 @@ -import { getCommonBounds } from "../../element"; +import { getCommonBounds, isTextElement } from "../../element"; +import { updateBoundElements } from "../../element/binding"; import { mutateElement } from "../../element/mutateElement"; import { rescalePointsInElement } from "../../element/resizeElements"; -import type { ExcalidrawElement } from "../../element/types"; +import { + getBoundTextElement, + handleBindTextResize, +} from "../../element/textElement"; +import type { ElementsMap, ExcalidrawElement } from "../../element/types"; import DragInput from "./DragInput"; import type { DragInputCallbackType } from "./DragInput"; import { getStepSizedValue } from "./utils"; @@ -9,6 +14,7 @@ import { getStepSizedValue } from "./utils"; interface MultiDimensionProps { property: "width" | "height"; elements: ExcalidrawElement[]; + elementsMap: ElementsMap; } const STEP_SIZE = 10; @@ -32,18 +38,66 @@ const getResizedUpdates = ( x, y, ...rescalePointsInElement(stateAtStart, nextWidth, nextHeight, false), + ...(isTextElement(stateAtStart) + ? { fontSize: stateAtStart.fontSize * scale } + : {}), }; }; -const MultiDimension = ({ property, elements }: MultiDimensionProps) => { - const handleDimensionChange: DragInputCallbackType = ( +const resizeElement = ( + anchorX: number, + anchorY: number, + property: MultiDimensionProps["property"], + scale: number, + latestElement: ExcalidrawElement, + origElement: ExcalidrawElement, + elementsMap: ElementsMap, + originalElementsMap: ElementsMap, + shouldInformMutation: boolean, +) => { + const updates = getResizedUpdates(anchorX, anchorY, scale, origElement); + + mutateElement(latestElement, updates, shouldInformMutation); + const boundTextElement = getBoundTextElement( + origElement, + originalElementsMap, + ); + if (boundTextElement) { + const newFontSize = boundTextElement.fontSize * scale; + updateBoundElements(latestElement, elementsMap, { + newSize: { width: updates.width, height: updates.height }, + }); + const latestBoundTextElement = elementsMap.get(boundTextElement.id); + if (latestBoundTextElement && isTextElement(latestBoundTextElement)) { + mutateElement( + latestBoundTextElement, + { + fontSize: newFontSize, + }, + shouldInformMutation, + ); + handleBindTextResize( + latestElement, + elementsMap, + property === "width" ? "e" : "s", + true, + ); + } + } +}; + +const MultiDimension = ({ + property, + elements, + elementsMap, +}: MultiDimensionProps) => { + const handleDimensionChange: DragInputCallbackType = ({ accumulatedChange, - instantChange, stateAtStart, - shouldKeepAspectRatio, + originalElementsMap, shouldChangeByStepSize, nextValue, - ) => { + }) => { const [x1, y1, x2, y2] = getCommonBounds(stateAtStart); const initialWidth = x2 - x1; const initialHeight = y2 - y1; @@ -60,15 +114,21 @@ const MultiDimension = ({ property, elements }: MultiDimensionProps) => { let i = 0; while (i < stateAtStart.length) { - const element = elements[i]; + const latestElement = elements[i]; const origElement = stateAtStart[i]; // it should never happen that element and origElement are different // but check just in case - if (element.id === origElement.id) { - mutateElement( - element, - getResizedUpdates(anchorX, anchorY, scale, origElement), + if (latestElement.id === origElement.id) { + resizeElement( + anchorX, + anchorY, + property, + scale, + latestElement, + origElement, + elementsMap, + originalElementsMap, i === stateAtStart.length - 1, ); } @@ -113,13 +173,19 @@ const MultiDimension = ({ property, elements }: MultiDimensionProps) => { let i = 0; while (i < stateAtStart.length) { - const element = elements[i]; + const latestElement = elements[i]; const origElement = stateAtStart[i]; - if (element.id === origElement.id) { - mutateElement( - element, - getResizedUpdates(anchorX, anchorY, scale, origElement), + if (latestElement.id === origElement.id) { + resizeElement( + anchorX, + anchorY, + property, + scale, + latestElement, + origElement, + elementsMap, + originalElementsMap, i === stateAtStart.length - 1, ); } diff --git a/packages/excalidraw/components/Stats/index.tsx b/packages/excalidraw/components/Stats/index.tsx index 60a9b73595..cf3858f4b8 100644 --- a/packages/excalidraw/components/Stats/index.tsx +++ b/packages/excalidraw/components/Stats/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { getCommonBounds } from "../../element/bounds"; import type { NonDeletedExcalidrawElement } from "../../element/types"; import { t } from "../../i18n"; -import { getTargetElements } from "../../scene"; +import { getSelectedElements } from "../../scene"; import type Scene from "../../scene/Scene"; import type { AppState, ExcalidrawProps } from "../../types"; import { CloseIcon } from "../icons"; @@ -29,7 +29,14 @@ export const Stats = (props: StatsProps) => { const elements = props.scene.getNonDeletedElements(); const elementsMap = props.scene.getNonDeletedElementsMap(); const sceneNonce = props.scene.getSceneNonce(); - const selectedElements = getTargetElements(elements, props.appState); + // const selectedElements = getTargetElements(elements, props.appState); + const selectedElements = getSelectedElements( + props.scene.getNonDeletedElementsMap(), + props.appState, + { + includeBoundTextElement: false, + }, + ); const singleElement = selectedElements.length === 1 ? selectedElements[0] : null; @@ -112,9 +119,17 @@ export const Stats = (props: StatsProps) => {
- - - + + + {singleElement.type === "text" && ( {
diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 002d1e94c6..b7602dfbf9 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -199,7 +199,7 @@ export const rescalePointsInElement = ( } : {}; -const measureFontSizeFromWidth = ( +export const measureFontSizeFromWidth = ( element: NonDeleted, elementsMap: ElementsMap, nextWidth: number,