feat(ui): add dimensions to params slice

This commit is contained in:
psychedelicious
2025-07-05 20:04:14 +10:00
parent 8d83caaae0
commit 0a737ced44
2 changed files with 163 additions and 2 deletions

View File

@@ -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<number>) => {
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;

View File

@@ -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<typeof zParamsState>;
const INITIAL_PARAMS_STATE = zParamsState.parse({});