diff options
Diffstat (limited to 'packages/excalidraw/components/Stats/Dimension.tsx')
| -rw-r--r-- | packages/excalidraw/components/Stats/Dimension.tsx | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx new file mode 100644 index 0000000..ce096d2 --- /dev/null +++ b/packages/excalidraw/components/Stats/Dimension.tsx @@ -0,0 +1,272 @@ +import type { ExcalidrawElement } from "../../element/types"; +import DragInput from "./DragInput"; +import type { DragInputCallbackType } from "./DragInput"; +import { getStepSizedValue, isPropertyEditable } from "./utils"; +import { MIN_WIDTH_OR_HEIGHT } from "../../constants"; +import { resizeSingleElement } from "../../element/resizeElements"; +import type Scene from "../../scene/Scene"; +import type { AppState } from "../../types"; +import { isImageElement } from "../../element/typeChecks"; +import { + MINIMAL_CROP_SIZE, + getUncroppedWidthAndHeight, +} from "../../element/cropElement"; +import { mutateElement } from "../../element/mutateElement"; +import { clamp, round } from "@excalidraw/math"; + +interface DimensionDragInputProps { + property: "width" | "height"; + element: ExcalidrawElement; + scene: Scene; + appState: AppState; +} + +const STEP_SIZE = 10; +const _shouldKeepAspectRatio = (element: ExcalidrawElement) => { + return element.type === "image"; +}; + +const handleDimensionChange: DragInputCallbackType< + DimensionDragInputProps["property"] +> = ({ + accumulatedChange, + originalElements, + originalElementsMap, + shouldKeepAspectRatio, + shouldChangeByStepSize, + nextValue, + property, + originalAppState, + instantChange, + scene, +}) => { + const elementsMap = scene.getNonDeletedElementsMap(); + const origElement = originalElements[0]; + const latestElement = elementsMap.get(origElement.id); + if (origElement && latestElement) { + const keepAspectRatio = + shouldKeepAspectRatio || _shouldKeepAspectRatio(origElement); + const aspectRatio = origElement.width / origElement.height; + + if (originalAppState.croppingElementId === origElement.id) { + const element = elementsMap.get(origElement.id); + + if (!element || !isImageElement(element) || !element.crop) { + return; + } + + const crop = element.crop; + let nextCrop = { ...crop }; + + const isFlippedByX = element.scale[0] === -1; + const isFlippedByY = element.scale[1] === -1; + + const { width: uncroppedWidth, height: uncroppedHeight } = + getUncroppedWidthAndHeight(element); + + const naturalToUncroppedWidthRatio = crop.naturalWidth / uncroppedWidth; + const naturalToUncroppedHeightRatio = + crop.naturalHeight / uncroppedHeight; + + const MAX_POSSIBLE_WIDTH = isFlippedByX + ? crop.width + crop.x + : crop.naturalWidth - crop.x; + + const MAX_POSSIBLE_HEIGHT = isFlippedByY + ? crop.height + crop.y + : crop.naturalHeight - crop.y; + + const MIN_WIDTH = MINIMAL_CROP_SIZE * naturalToUncroppedWidthRatio; + const MIN_HEIGHT = MINIMAL_CROP_SIZE * naturalToUncroppedHeightRatio; + + if (nextValue !== undefined) { + if (property === "width") { + const nextValueInNatural = nextValue * naturalToUncroppedWidthRatio; + + const nextCropWidth = clamp( + nextValueInNatural, + MIN_WIDTH, + MAX_POSSIBLE_WIDTH, + ); + + nextCrop = { + ...nextCrop, + width: nextCropWidth, + x: isFlippedByX ? crop.x + crop.width - nextCropWidth : crop.x, + }; + } else if (property === "height") { + const nextValueInNatural = nextValue * naturalToUncroppedHeightRatio; + const nextCropHeight = clamp( + nextValueInNatural, + MIN_HEIGHT, + MAX_POSSIBLE_HEIGHT, + ); + + nextCrop = { + ...nextCrop, + height: nextCropHeight, + y: isFlippedByY ? crop.y + crop.height - nextCropHeight : crop.y, + }; + } + + mutateElement(element, { + crop: nextCrop, + width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), + height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), + }); + return; + } + + const changeInWidth = property === "width" ? instantChange : 0; + const changeInHeight = property === "height" ? instantChange : 0; + + const nextCropWidth = clamp( + crop.width + changeInWidth, + MIN_WIDTH, + MAX_POSSIBLE_WIDTH, + ); + + const nextCropHeight = clamp( + crop.height + changeInHeight, + MIN_WIDTH, + MAX_POSSIBLE_HEIGHT, + ); + + nextCrop = { + ...crop, + x: isFlippedByX ? crop.x + crop.width - nextCropWidth : crop.x, + y: isFlippedByY ? crop.y + crop.height - nextCropHeight : crop.y, + width: nextCropWidth, + height: nextCropHeight, + }; + + mutateElement(element, { + crop: nextCrop, + width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), + height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), + }); + + return; + } + + if (nextValue !== undefined) { + const nextWidth = Math.max( + property === "width" + ? nextValue + : keepAspectRatio + ? nextValue * aspectRatio + : origElement.width, + MIN_WIDTH_OR_HEIGHT, + ); + const nextHeight = Math.max( + property === "height" + ? nextValue + : keepAspectRatio + ? nextValue / aspectRatio + : origElement.height, + MIN_WIDTH_OR_HEIGHT, + ); + + resizeSingleElement( + nextWidth, + nextHeight, + latestElement, + origElement, + elementsMap, + originalElementsMap, + property === "width" ? "e" : "s", + { + shouldMaintainAspectRatio: keepAspectRatio, + }, + ); + + return; + } + const changeInWidth = property === "width" ? accumulatedChange : 0; + const changeInHeight = property === "height" ? accumulatedChange : 0; + + let nextWidth = Math.max(0, origElement.width + changeInWidth); + if (property === "width") { + if (shouldChangeByStepSize) { + nextWidth = getStepSizedValue(nextWidth, STEP_SIZE); + } else { + nextWidth = Math.round(nextWidth); + } + } + + let nextHeight = Math.max(0, origElement.height + 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; + } + } + + nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight); + nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth); + + resizeSingleElement( + nextWidth, + nextHeight, + latestElement, + origElement, + elementsMap, + originalElementsMap, + property === "width" ? "e" : "s", + { + shouldMaintainAspectRatio: keepAspectRatio, + }, + ); + } +}; + +const DimensionDragInput = ({ + property, + element, + scene, + appState, +}: DimensionDragInputProps) => { + let value = round(property === "width" ? element.width : element.height, 2); + + if ( + appState.croppingElementId && + appState.croppingElementId === element.id && + isImageElement(element) && + element.crop + ) { + const { width: uncroppedWidth, height: uncroppedHeight } = + getUncroppedWidthAndHeight(element); + if (property === "width") { + const ratio = uncroppedWidth / element.crop.naturalWidth; + value = round(element.crop.width * ratio, 2); + } + if (property === "height") { + const ratio = uncroppedHeight / element.crop.naturalHeight; + value = round(element.crop.height * ratio, 2); + } + } + + return ( + <DragInput + label={property === "width" ? "W" : "H"} + elements={[element]} + dragInputCallback={handleDimensionChange} + value={value} + editable={isPropertyEditable(element, property)} + scene={scene} + appState={appState} + property={property} + /> + ); +}; + +export default DimensionDragInput; |
