diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 371719e087..ae184aec00 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -7,7 +7,14 @@ import { import { IRect, Vector2d } from 'konva/lib/types'; import { clamp, cloneDeep } from 'lodash-es'; // +import { + setActiveTab, + setAspectRatio, + setShouldUseCanvasBetaLayout, +} from 'features/ui/store/uiSlice'; import { RgbaColor } from 'react-colorful'; +import { sessionCanceled } from 'services/api/thunks/session'; +import { ImageDTO } from 'services/api/types'; import calculateCoordinates from '../util/calculateCoordinates'; import calculateScale from '../util/calculateScale'; import { STAGE_PADDING_PERCENTAGE } from '../util/constants'; @@ -28,13 +35,6 @@ import { isCanvasBaseImage, isCanvasMaskLine, } from './canvasTypes'; -import { ImageDTO } from 'services/api/types'; -import { sessionCanceled } from 'services/api/thunks/session'; -import { - setActiveTab, - setShouldUseCanvasBetaLayout, -} from 'features/ui/store/uiSlice'; -import { imageUrlsReceived } from 'services/api/thunks/image'; export const initialLayerState: CanvasLayerState = { objects: [], @@ -240,6 +240,16 @@ export const canvasSlice = createSlice({ state.scaledBoundingBoxDimensions = scaledDimensions; } }, + toggleBoundingBoxDimensions: (state) => { + const [currWidth, currHeight] = [ + state.boundingBoxDimensions.width, + state.boundingBoxDimensions.height, + ]; + state.boundingBoxDimensions = { + width: currHeight, + height: currWidth, + }; + }, setBoundingBoxCoordinates: (state, action: PayloadAction) => { state.boundingBoxCoordinates = floorCoordinates(action.payload); }, @@ -864,6 +874,15 @@ export const canvasSlice = createSlice({ builder.addCase(setActiveTab, (state, action) => { state.doesCanvasNeedScaling = true; }); + builder.addCase(setAspectRatio, (state, action) => { + const ratio = action.payload; + if (ratio) { + state.boundingBoxDimensions.height = roundToMultiple( + state.boundingBoxDimensions.width / ratio, + 8 + ); + } + }); // builder.addCase(imageUrlsReceived.fulfilled, (state, action) => { // const { image_name, image_url, thumbnail_url } = action.payload; @@ -912,6 +931,7 @@ export const { setBoundingBoxDimensions, setBoundingBoxPreviewFill, setBoundingBoxScaleMethod, + toggleBoundingBoxDimensions, setBrushColor, setBrushSize, setCanvasContainerDimensions, diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx index b529c87225..78fa245753 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx @@ -2,22 +2,26 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider from 'common/components/IAISlider'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { canvasSelector, isStagingSelector, } from 'features/canvas/store/canvasSelectors'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { uiSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - [canvasSelector, isStagingSelector], - (canvas, isStaging) => { + [canvasSelector, isStagingSelector, uiSelector], + (canvas, isStaging, ui) => { const { boundingBoxDimensions } = canvas; + const { aspectRatio } = ui; return { boundingBoxDimensions, isStaging, + aspectRatio, }; }, defaultSelectorOptions @@ -25,7 +29,8 @@ const selector = createSelector( const ParamBoundingBoxWidth = () => { const dispatch = useAppDispatch(); - const { boundingBoxDimensions, isStaging } = useAppSelector(selector); + const { boundingBoxDimensions, isStaging, aspectRatio } = + useAppSelector(selector); const { t } = useTranslation(); @@ -36,6 +41,15 @@ const ParamBoundingBoxWidth = () => { height: Math.floor(v), }) ); + if (aspectRatio) { + const newWidth = roundToMultiple(v * aspectRatio, 8); + dispatch( + setBoundingBoxDimensions({ + width: newWidth, + height: Math.floor(v), + }) + ); + } }; const handleResetHeight = () => { @@ -45,6 +59,15 @@ const ParamBoundingBoxWidth = () => { height: Math.floor(512), }) ); + if (aspectRatio) { + const newWidth = roundToMultiple(512 * aspectRatio, 8); + dispatch( + setBoundingBoxDimensions({ + width: newWidth, + height: Math.floor(512), + }) + ); + } }; return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxSize.tsx new file mode 100644 index 0000000000..6c0ab0cd85 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxSize.tsx @@ -0,0 +1,57 @@ +import { Flex, Spacer, Text } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/store/storeHooks'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { toggleBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { useTranslation } from 'react-i18next'; +import { MdOutlineSwapVert } from 'react-icons/md'; +import ParamAspectRatio from '../../Core/ParamAspectRatio'; +import ParamBoundingBoxHeight from './ParamBoundingBoxHeight'; +import ParamBoundingBoxWidth from './ParamBoundingBoxWidth'; + +export default function ParamBoundingBoxSize() { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + return ( + + + + {t('parameters.aspectRatio')} + + + + } + fontSize={20} + onClick={() => dispatch(toggleBoundingBoxDimensions())} + /> + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx index 07d74049ed..69edd09137 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx @@ -2,22 +2,26 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider from 'common/components/IAISlider'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { canvasSelector, isStagingSelector, } from 'features/canvas/store/canvasSelectors'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { uiSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createSelector( - [canvasSelector, isStagingSelector], - (canvas, isStaging) => { + [canvasSelector, isStagingSelector, uiSelector], + (canvas, isStaging, ui) => { const { boundingBoxDimensions } = canvas; + const { aspectRatio } = ui; return { boundingBoxDimensions, isStaging, + aspectRatio, }; }, defaultSelectorOptions @@ -25,7 +29,8 @@ const selector = createSelector( const ParamBoundingBoxWidth = () => { const dispatch = useAppDispatch(); - const { boundingBoxDimensions, isStaging } = useAppSelector(selector); + const { boundingBoxDimensions, isStaging, aspectRatio } = + useAppSelector(selector); const { t } = useTranslation(); @@ -36,6 +41,15 @@ const ParamBoundingBoxWidth = () => { width: Math.floor(v), }) ); + if (aspectRatio) { + const newHeight = roundToMultiple(v / aspectRatio, 8); + dispatch( + setBoundingBoxDimensions({ + width: Math.floor(v), + height: newHeight, + }) + ); + } }; const handleResetWidth = () => { @@ -45,6 +59,15 @@ const ParamBoundingBoxWidth = () => { width: Math.floor(512), }) ); + if (aspectRatio) { + const newHeight = roundToMultiple(512 / aspectRatio, 8); + dispatch( + setBoundingBoxDimensions({ + width: Math.floor(512), + height: newHeight, + }) + ); + } }; return ( diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx index 6ea9d4bc8d..6e4ce7d5d0 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx @@ -4,8 +4,7 @@ import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAICollapse from 'common/components/IAICollapse'; -import ParamBoundingBoxHeight from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight'; -import ParamBoundingBoxWidth from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth'; +import ParamBoundingBoxSize from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxSize'; import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; import ParamModelandVAEandScheduler from 'features/parameters/components/Parameters/Core/ParamModelandVAEandScheduler'; @@ -51,8 +50,7 @@ const UnifiedCanvasCoreParameters = () => { - - + ) : ( <> @@ -65,8 +63,7 @@ const UnifiedCanvasCoreParameters = () => { - - + )}