diff --git a/packages/excalidraw/components/Stats/Angle.tsx b/packages/excalidraw/components/Stats/Angle.tsx index 225368e546..c0c303c7ff 100644 --- a/packages/excalidraw/components/Stats/Angle.tsx +++ b/packages/excalidraw/components/Stats/Angle.tsx @@ -20,17 +20,18 @@ const Angle = ({ element }: AngleProps) => { shouldChangeByStepSize, nextValue, ) => { - if (nextValue !== undefined) { - const nextAngle = degreeToRadian(nextValue); - mutateElement(element, { - angle: nextAngle, - }); - return; - } + const _stateAtStart = stateAtStart[0]; + if (_stateAtStart) { + if (nextValue !== undefined) { + const nextAngle = degreeToRadian(nextValue); + mutateElement(element, { + angle: nextAngle, + }); + return; + } - if (stateAtStart) { const originalAngleInDegrees = - Math.round(radianToDegree(stateAtStart.angle) * 100) / 100; + Math.round(radianToDegree(_stateAtStart.angle) * 100) / 100; const changeInDegrees = Math.round(accumulatedChange); let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360; if (shouldChangeByStepSize) { @@ -51,7 +52,7 @@ const Angle = ({ element }: AngleProps) => { diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx index 448c4178cf..a93c2da5b6 100644 --- a/packages/excalidraw/components/Stats/Dimension.tsx +++ b/packages/excalidraw/components/Stats/Dimension.tsx @@ -14,7 +14,7 @@ const _shouldKeepAspectRatio = (element: ExcalidrawElement) => { return element.type === "image"; }; -const newOrigin = ( +export const newOrigin = ( x1: number, y1: number, w1: number, @@ -59,10 +59,11 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { shouldChangeByStepSize, nextValue, ) => { - if (stateAtStart) { + const _stateAtStart = stateAtStart[0]; + if (_stateAtStart) { const keepAspectRatio = shouldKeepAspectRatio || _shouldKeepAspectRatio(element); - const aspectRatio = stateAtStart.width / stateAtStart.height; + const aspectRatio = _stateAtStart.width / _stateAtStart.height; if (nextValue !== undefined) { const nextWidth = Math.max( @@ -70,7 +71,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { ? nextValue : keepAspectRatio ? nextValue * aspectRatio - : stateAtStart.width, + : _stateAtStart.width, 0, ); const nextHeight = Math.max( @@ -78,7 +79,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { ? nextValue : keepAspectRatio ? nextValue / aspectRatio - : stateAtStart.height, + : _stateAtStart.height, 0, ); @@ -100,7 +101,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { const changeInWidth = property === "width" ? accumulatedChange : 0; const changeInHeight = property === "height" ? accumulatedChange : 0; - let nextWidth = Math.max(0, stateAtStart.width + changeInWidth); + let nextWidth = Math.max(0, _stateAtStart.width + changeInWidth); if (property === "width") { if (shouldChangeByStepSize) { nextWidth = getStepSizedValue(nextWidth, STEP_SIZE); @@ -109,7 +110,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { } } - let nextHeight = Math.max(0, stateAtStart.height + changeInHeight); + let nextHeight = Math.max(0, _stateAtStart.height + changeInHeight); if (property === "height") { if (shouldChangeByStepSize) { nextHeight = getStepSizedValue(nextHeight, STEP_SIZE); @@ -130,13 +131,13 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { width: nextWidth, height: nextHeight, ...newOrigin( - stateAtStart.x, - stateAtStart.y, - stateAtStart.width, - stateAtStart.height, + _stateAtStart.x, + _stateAtStart.y, + _stateAtStart.width, + _stateAtStart.height, nextWidth, nextHeight, - stateAtStart.angle, + _stateAtStart.angle, ), }); } @@ -145,7 +146,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => { return ( { @@ -64,7 +64,7 @@ const StatsDragInput = ({ y: number; } | null = null; - let stateAtStart: ExcalidrawElement | null = null; + let stateAtStart: ExcalidrawElement[] | null = null; let accumulatedChange: number | null = null; @@ -72,7 +72,9 @@ const StatsDragInput = ({ const onPointerMove = (event: PointerEvent) => { if (!stateAtStart) { - stateAtStart = deepCopyElement(element); + stateAtStart = elements.map((element) => + deepCopyElement(element), + ); } if (!accumulatedChange) { @@ -146,11 +148,12 @@ const StatsDragInput = ({ dragInputCallback( 0, 0, - element, + elements, shouldKeepAspectRatio!!, false, v, ); + eventTarget.blur(); } } }} diff --git a/packages/excalidraw/components/Stats/FontSize.tsx b/packages/excalidraw/components/Stats/FontSize.tsx index 873e78373a..724fced230 100644 --- a/packages/excalidraw/components/Stats/FontSize.tsx +++ b/packages/excalidraw/components/Stats/FontSize.tsx @@ -22,40 +22,43 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => { shouldChangeByStepSize, nextValue, ) => { - if (nextValue) { - const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE); + const _stateAtStart = stateAtStart[0]; + if (_stateAtStart) { + if (nextValue) { + const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE); - const newElement = { - ...element, - fontSize: nextFontSize, - }; - const updates = refreshTextDimensions(newElement, null, elementsMap); - mutateElement(element, { - ...updates, - fontSize: nextFontSize, - }); - return; - } + const newElement = { + ...element, + fontSize: nextFontSize, + }; + const updates = refreshTextDimensions(newElement, null, elementsMap); + mutateElement(element, { + ...updates, + fontSize: nextFontSize, + }); + return; + } - if (stateAtStart && stateAtStart.type === "text") { - const originalFontSize = Math.round(stateAtStart.fontSize); - const changeInFontSize = Math.round(accumulatedChange); - let nextFontSize = Math.max( - originalFontSize + changeInFontSize, - MIN_FONT_SIZE, - ); - if (shouldChangeByStepSize) { - nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE); + if (_stateAtStart.type === "text") { + const originalFontSize = Math.round(_stateAtStart.fontSize); + const changeInFontSize = Math.round(accumulatedChange); + let nextFontSize = Math.max( + originalFontSize + changeInFontSize, + MIN_FONT_SIZE, + ); + if (shouldChangeByStepSize) { + nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE); + } + const newElement = { + ...element, + fontSize: nextFontSize, + }; + const updates = refreshTextDimensions(newElement, null, elementsMap); + mutateElement(element, { + ...updates, + fontSize: nextFontSize, + }); } - const newElement = { - ...element, - fontSize: nextFontSize, - }; - const updates = refreshTextDimensions(newElement, null, elementsMap); - mutateElement(element, { - ...updates, - fontSize: nextFontSize, - }); } }; @@ -63,7 +66,7 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => { ); diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx new file mode 100644 index 0000000000..081e4731c9 --- /dev/null +++ b/packages/excalidraw/components/Stats/MultiDimension.tsx @@ -0,0 +1,143 @@ +import { getCommonBounds } from "../../element"; +import { mutateElement } from "../../element/mutateElement"; +import type { ExcalidrawElement } from "../../element/types"; +import DragInput from "./DragInput"; +import type { DragInputCallbackType } from "./DragInput"; +import { getStepSizedValue } from "./utils"; + +interface MultiDimensionProps { + property: "width" | "height"; + elements: ExcalidrawElement[]; +} + +const STEP_SIZE = 10; + +const MultiDimension = ({ property, elements }: MultiDimensionProps) => { + const handleDimensionChange: DragInputCallbackType = ( + accumulatedChange, + instantChange, + stateAtStart, + shouldKeepAspectRatio, + shouldChangeByStepSize, + nextValue, + ) => { + const [x1, y1, x2, y2] = getCommonBounds(stateAtStart); + const initialWidth = x2 - x1; + const initialHeight = y2 - y1; + const keepAspectRatio = true; + const aspectRatio = initialWidth / initialHeight; + + if (nextValue !== undefined) { + const nextHeight = + property === "height" ? nextValue : nextValue / aspectRatio; + + const scale = nextHeight / initialHeight; + const anchorX = property === "width" ? x1 : x1 + width / 2; + const anchorY = property === "height" ? y1 : y1 + height / 2; + + let i = 0; + while (i < stateAtStart.length) { + const element = 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) { + const offsetX = origElement.x - anchorX; + const offsetY = origElement.y - anchorY; + const nextWidth = origElement.width * scale; + const nextHeight = origElement.height * scale; + const x = anchorX + offsetX * scale; + const y = anchorY + offsetY * scale; + + mutateElement( + element, + { + width: nextWidth, + height: nextHeight, + x, + y, + }, + i === stateAtStart.length - 1, + ); + } + i++; + } + + return; + } + + const changeInWidth = property === "width" ? accumulatedChange : 0; + const changeInHeight = property === "height" ? accumulatedChange : 0; + + let nextWidth = Math.max(0, initialWidth + changeInWidth); + if (property === "width") { + if (shouldChangeByStepSize) { + nextWidth = getStepSizedValue(nextWidth, STEP_SIZE); + } else { + nextWidth = Math.round(nextWidth); + } + } + + let nextHeight = Math.max(0, initialHeight + changeInHeight); + if (property === "height") { + if (shouldChangeByStepSize) { + nextHeight = getStepSizedValue(nextHeight, STEP_SIZE); + } else { + nextHeight = Math.round(nextHeight); + } + } + + if (keepAspectRatio) { + if (property === "width") { + nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100; + } else { + nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100; + } + } + + const scale = nextHeight / initialHeight; + const anchorX = property === "width" ? x1 : x1 + width / 2; + const anchorY = property === "height" ? y1 : y1 + height / 2; + + let i = 0; + while (i < stateAtStart.length) { + const element = elements[i]; + const origElement = stateAtStart[i]; + + const offsetX = origElement.x - anchorX; + const offsetY = origElement.y - anchorY; + const nextWidth = origElement.width * scale; + const nextHeight = origElement.height * scale; + const x = anchorX + offsetX * scale; + const y = anchorY + offsetY * scale; + + mutateElement( + element, + { + width: nextWidth, + height: nextHeight, + x, + y, + }, + i === stateAtStart.length - 1, + ); + i++; + } + }; + + const [x1, y1, x2, y2] = getCommonBounds(elements); + const width = x2 - x1; + const height = y2 - y1; + + return ( + + ); +}; + +export default MultiDimension; diff --git a/packages/excalidraw/components/Stats/index.scss b/packages/excalidraw/components/Stats/index.scss index ed52788966..2a6d9a113b 100644 --- a/packages/excalidraw/components/Stats/index.scss +++ b/packages/excalidraw/components/Stats/index.scss @@ -23,6 +23,14 @@ margin-bottom: 8px; } + .elementsCount { + width: 100%; + font-size: 12px; + display: flex; + justify-content: space-between; + margin-bottom: 12px; + } + .statsItem { width: 100%; margin-bottom: 4px; diff --git a/packages/excalidraw/components/Stats/index.tsx b/packages/excalidraw/components/Stats/index.tsx index 27e54055ee..60a9b73595 100644 --- a/packages/excalidraw/components/Stats/index.tsx +++ b/packages/excalidraw/components/Stats/index.tsx @@ -13,6 +13,8 @@ import Angle from "./Angle"; import "./index.scss"; import FontSize from "./FontSize"; +import MultiDimension from "./MultiDimension"; +import { elementsAreInSameGroup } from "../../groups"; interface StatsProps { appState: AppState; @@ -32,6 +34,9 @@ export const Stats = (props: StatsProps) => { const singleElement = selectedElements.length === 1 ? selectedElements[0] : null; + const multipleElements = + selectedElements.length > 1 ? selectedElements : null; + const [sceneDimension, setSceneDimension] = useState<{ width: number; height: number; @@ -91,7 +96,7 @@ export const Stats = (props: StatsProps) => { - {singleElement && ( + {selectedElements.length > 0 && (
{ >

{t("stats.elementStats")}

-
-
- {t(`element.${singleElement.type}`)} + {singleElement && ( +
+
+ {t(`element.${singleElement.type}`)} +
+ +
+ + + + {singleElement.type === "text" && ( + + )} +
+ + {singleElement.type === "text" &&
}
+ )} -
- - - - {singleElement.type === "text" && ( - + {multipleElements && ( +
+ {elementsAreInSameGroup(multipleElements) && ( +
{t("element.group")}
)} -
- {singleElement.type === "text" &&
} -
+
+
{t("stats.elements")}
+
{selectedElements.length}
+
+ +
+ + +
+
+ )}
)}