From 0a737ced44eeeb07f872d34f3b41559d53560e74 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 5 Jul 2025 20:04:14 +1000 Subject: [PATCH] feat(ui): add dimensions to params slice --- .../controlLayers/store/paramsSlice.ts | 151 +++++++++++++++++- .../src/features/controlLayers/store/types.ts | 14 ++ 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index f655ce7277..9ee4633e1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,9 +1,22 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; +import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; import { clamp } from 'es-toolkit/compat'; -import type { ParamsState, RgbaColor } from 'features/controlLayers/store/types'; -import { getInitialParamsState } from 'features/controlLayers/store/types'; +import type { AspectRatioID, ParamsState, RgbaColor } from 'features/controlLayers/store/types'; +import { + ASPECT_RATIO_MAP, + CHATGPT_ASPECT_RATIOS, + DEFAULT_ASPECT_RATIO_CONFIG, + FLUX_KONTEXT_ASPECT_RATIOS, + getInitialParamsState, + IMAGEN_ASPECT_RATIOS, + isChatGPT4oAspectRatioID, + isFluxKontextAspectRatioID, + isImagenAspectRatioID, +} from 'features/controlLayers/store/types'; +import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import type { ParameterCanvasCoherenceMode, @@ -23,6 +36,7 @@ import type { ParameterT5EncoderModel, ParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; +import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import { isNonRefinerMainModelConfig } from 'services/api/types'; @@ -186,6 +200,129 @@ export const paramsSlice = createSlice({ setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { state.canvasCoherenceMinDenoise = action.payload; }, + + //#region Dimensions + widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { + const { width, updateAspectRatio, clamp } = action.payload; + const gridSize = getGridSize(state.model?.base); + state.dimensions.rect.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; + + if (state.dimensions.aspectRatio.isLocked) { + state.dimensions.rect.height = roundToMultiple( + state.dimensions.rect.width / state.dimensions.aspectRatio.value, + gridSize + ); + } + + if (updateAspectRatio || !state.dimensions.aspectRatio.isLocked) { + state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height; + state.dimensions.aspectRatio.id = 'Free'; + state.dimensions.aspectRatio.isLocked = false; + } + }, + heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { + const { height, updateAspectRatio, clamp } = action.payload; + const gridSize = getGridSize(state.model?.base); + state.dimensions.rect.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; + + if (state.dimensions.aspectRatio.isLocked) { + state.dimensions.rect.width = roundToMultiple( + state.dimensions.rect.height * state.dimensions.aspectRatio.value, + gridSize + ); + } + + if (updateAspectRatio || !state.dimensions.aspectRatio.isLocked) { + state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height; + state.dimensions.aspectRatio.id = 'Free'; + state.dimensions.aspectRatio.isLocked = false; + } + }, + aspectRatioLockToggled: (state) => { + state.dimensions.aspectRatio.isLocked = !state.dimensions.aspectRatio.isLocked; + }, + aspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { + const { id } = action.payload; + state.dimensions.aspectRatio.id = id; + if (id === 'Free') { + state.dimensions.aspectRatio.isLocked = false; + } else if ((state.model?.base === 'imagen3' || state.model?.base === 'imagen4') && isImagenAspectRatioID(id)) { + const { width, height } = IMAGEN_ASPECT_RATIOS[id]; + state.dimensions.rect.width = width; + state.dimensions.rect.height = height; + state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height; + state.dimensions.aspectRatio.isLocked = true; + } else if (state.model?.base === 'chatgpt-4o' && isChatGPT4oAspectRatioID(id)) { + const { width, height } = CHATGPT_ASPECT_RATIOS[id]; + state.dimensions.rect.width = width; + state.dimensions.rect.height = height; + state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height; + state.dimensions.aspectRatio.isLocked = true; + } else if (state.model?.base === 'flux-kontext' && isFluxKontextAspectRatioID(id)) { + const { width, height } = FLUX_KONTEXT_ASPECT_RATIOS[id]; + state.dimensions.rect.width = width; + state.dimensions.rect.height = height; + state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height; + state.dimensions.aspectRatio.isLocked = true; + } else { + state.dimensions.aspectRatio.isLocked = true; + state.dimensions.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + const { width, height } = calculateNewSize( + state.dimensions.aspectRatio.value, + state.dimensions.rect.width * state.dimensions.rect.height, + state.model?.base + ); + state.dimensions.rect.width = width; + state.dimensions.rect.height = height; + } + }, + dimensionsSwapped: (state) => { + state.dimensions.aspectRatio.value = 1 / state.dimensions.aspectRatio.value; + if (state.dimensions.aspectRatio.id === 'Free') { + const newWidth = state.dimensions.rect.height; + const newHeight = state.dimensions.rect.width; + state.dimensions.rect.width = newWidth; + state.dimensions.rect.height = newHeight; + } else { + const { width, height } = calculateNewSize( + state.dimensions.aspectRatio.value, + state.dimensions.rect.width * state.dimensions.rect.height, + state.model?.base + ); + state.dimensions.rect.width = width; + state.dimensions.rect.height = height; + state.dimensions.aspectRatio.id = ASPECT_RATIO_MAP[state.dimensions.aspectRatio.id].inverseID; + } + }, + sizeOptimized: (state) => { + const optimalDimension = getOptimalDimension(state.model?.base); + if (state.dimensions.aspectRatio.isLocked) { + const { width, height } = calculateNewSize( + state.dimensions.aspectRatio.value, + optimalDimension * optimalDimension, + state.model?.base + ); + state.dimensions.rect.width = width; + state.dimensions.rect.height = height; + } else { + state.dimensions.aspectRatio = deepClone(DEFAULT_ASPECT_RATIO_CONFIG); + state.dimensions.rect.width = optimalDimension; + state.dimensions.rect.height = optimalDimension; + } + }, + syncedToOptimalDimension: (state) => { + const optimalDimension = getOptimalDimension(state.model?.base); + + if (!getIsSizeOptimal(state.dimensions.rect.width, state.dimensions.rect.height, state.model?.base)) { + const bboxDims = calculateNewSize( + state.dimensions.aspectRatio.value, + optimalDimension * optimalDimension, + state.model?.base + ); + state.dimensions.rect.width = bboxDims.width; + state.dimensions.rect.height = bboxDims.height; + } + }, paramsReset: (state) => resetState(state), }, }); @@ -249,6 +386,16 @@ export const { setRefinerNegativeAestheticScore, setRefinerStart, modelChanged, + + // Dimensions + widthChanged, + heightChanged, + aspectRatioLockToggled, + aspectRatioIdChanged, + dimensionsSwapped, + sizeOptimized, + syncedToOptimalDimension, + paramsReset, } = paramsSlice.actions; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 59f8f05679..cb6f3a9477 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -512,6 +512,16 @@ const zBboxState = z.object({ modelBase: zMainModelBase, }); +const zDimensionsState = z.object({ + rect: z.object({ + x: z.number().int(), + y: z.number().int(), + width: zParameterImageDimension, + height: zParameterImageDimension, + }), + aspectRatio: zAspectRatioConfig, +}); + const zParamsState = z.object({ maskBlur: z.number().default(16), maskBlurMethod: zParameterMaskBlurMethod.default('box'), @@ -560,6 +570,10 @@ const zParamsState = z.object({ clipLEmbedModel: zParameterCLIPLEmbedModel.nullable().default(null), clipGEmbedModel: zParameterCLIPGEmbedModel.nullable().default(null), controlLora: zParameterControlLoRAModel.nullable().default(null), + dimensions: zDimensionsState.default({ + rect: { x: 0, y: 0, width: 512, height: 512 }, + aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG, + }), }); export type ParamsState = z.infer; const INITIAL_PARAMS_STATE = zParamsState.parse({});