Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@

- [ ] _The PR has a short but descriptive title, suitable for a changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _❗Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
21 changes: 10 additions & 11 deletions invokeai/frontend/web/src/app/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware
import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings';
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected';
import { deepClone } from 'common/util/deepClone';
import { keys, mergeWith, omit, pick } from 'es-toolkit/compat';
import { merge } from 'es-toolkit';
import { omit, pick } from 'es-toolkit/compat';
import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice';
import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice';
Expand Down Expand Up @@ -133,16 +134,14 @@ const unserialize: UnserializeFunction = (data, key) => {
const initialState = getInitialState();
const parsed = JSON.parse(data);

// strip out old keys
const stripped = pick(deepClone(parsed), keys(initialState));
/*
* Merge in initial state as default values, covering any missing keys. You might be tempted to use _.defaultsDeep,
* but that merges arrays by index and partial objects by key. Using an identity function as the customizer results
* in behaviour like defaultsDeep, but doesn't overwrite any values that are not undefined in the migrated state.
*/
const unPersistDenylisted = mergeWith(stripped, initialState, (objVal) => objVal);
// run (additive) migrations
const migrated = persistConfig.migrate(unPersistDenylisted);
// We need to inject non-persisted values from initial state into the rehydrated state. These values always are
// required to be in the state, but won't be in the persisted data. Build an object that consists of only these
// values, then merge it with the rehydrated state.
const nonPersistedSubsetOfState = pick(initialState, persistConfig.persistDenylist ?? []);
const stateToMigrate = merge(deepClone(parsed), nonPersistedSubsetOfState);

// Run migrations to bring old state up to date with the current version.
const migrated = persistConfig.migrate(stateToMigrate);

log.debug(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RootState } from 'app/store/store';
import type { SliceConfig } from 'app/store/types';
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import { isPlainObject } from 'es-toolkit';
import { clamp } from 'es-toolkit/compat';
import type { AspectRatioID, ParamsState, RgbaColor } from 'features/controlLayers/store/types';
import {
Expand Down Expand Up @@ -52,6 +53,7 @@ import type {
import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
import { isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';

const slice = createSlice({
name: 'params',
Expand Down Expand Up @@ -122,8 +124,8 @@ const slice = createSlice({
state.dimensions.aspectRatio.isLocked = true;
state.dimensions.aspectRatio.value = 1;
state.dimensions.aspectRatio.id = '1:1';
state.dimensions.rect.width = 1024;
state.dimensions.rect.height = 1024;
state.dimensions.width = 1024;
state.dimensions.height = 1024;
}

applyClipSkip(state, model, state.clipSkip);
Expand Down Expand Up @@ -247,44 +249,44 @@ const slice = createSlice({
sizeRecalled: (state, action: PayloadAction<{ width: number; height: number }>) => {
const { width, height } = action.payload;
const gridSize = getGridSize(state.model?.base);
state.dimensions.rect.width = Math.max(roundDownToMultiple(width, gridSize), 64);
state.dimensions.rect.height = Math.max(roundDownToMultiple(height, gridSize), 64);
state.dimensions.aspectRatio.value = state.dimensions.rect.width / state.dimensions.rect.height;
state.dimensions.width = Math.max(roundDownToMultiple(width, gridSize), 64);
state.dimensions.height = Math.max(roundDownToMultiple(height, gridSize), 64);
state.dimensions.aspectRatio.value = state.dimensions.width / state.dimensions.height;
state.dimensions.aspectRatio.id = 'Free';
state.dimensions.aspectRatio.isLocked = true;
},
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;
state.dimensions.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,
state.dimensions.height = roundToMultiple(
state.dimensions.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.value = state.dimensions.width / state.dimensions.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;
state.dimensions.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,
state.dimensions.width = roundToMultiple(
state.dimensions.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.value = state.dimensions.width / state.dimensions.height;
state.dimensions.aspectRatio.id = 'Free';
state.dimensions.aspectRatio.isLocked = false;
}
Expand All @@ -299,55 +301,55 @@ const slice = createSlice({
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.width = width;
state.dimensions.height = height;
state.dimensions.aspectRatio.value = state.dimensions.width / state.dimensions.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.width = width;
state.dimensions.height = height;
state.dimensions.aspectRatio.value = state.dimensions.width / state.dimensions.height;
state.dimensions.aspectRatio.isLocked = true;
} else if (state.model?.base === 'gemini-2.5' && isGemini2_5AspectRatioID(id)) {
const { width, height } = GEMINI_2_5_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.width = width;
state.dimensions.height = height;
state.dimensions.aspectRatio.value = state.dimensions.width / state.dimensions.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.width = width;
state.dimensions.height = height;
state.dimensions.aspectRatio.value = state.dimensions.width / state.dimensions.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.dimensions.width * state.dimensions.height,
state.model?.base
);
state.dimensions.rect.width = width;
state.dimensions.rect.height = height;
state.dimensions.width = width;
state.dimensions.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;
const newWidth = state.dimensions.height;
const newHeight = state.dimensions.width;
state.dimensions.width = newWidth;
state.dimensions.height = newHeight;
} else {
const { width, height } = calculateNewSize(
state.dimensions.aspectRatio.value,
state.dimensions.rect.width * state.dimensions.rect.height,
state.dimensions.width * state.dimensions.height,
state.model?.base
);
state.dimensions.rect.width = width;
state.dimensions.rect.height = height;
state.dimensions.width = width;
state.dimensions.height = height;
state.dimensions.aspectRatio.id = ASPECT_RATIO_MAP[state.dimensions.aspectRatio.id].inverseID;
}
},
Expand All @@ -359,25 +361,25 @@ const slice = createSlice({
optimalDimension * optimalDimension,
state.model?.base
);
state.dimensions.rect.width = width;
state.dimensions.rect.height = height;
state.dimensions.width = width;
state.dimensions.height = height;
} else {
state.dimensions.aspectRatio = deepClone(DEFAULT_ASPECT_RATIO_CONFIG);
state.dimensions.rect.width = optimalDimension;
state.dimensions.rect.height = optimalDimension;
state.dimensions.width = optimalDimension;
state.dimensions.height = optimalDimension;
}
},
syncedToOptimalDimension: (state) => {
const optimalDimension = getOptimalDimension(state.model?.base);

if (!getIsSizeOptimal(state.dimensions.rect.width, state.dimensions.rect.height, state.model?.base)) {
if (!getIsSizeOptimal(state.dimensions.width, state.dimensions.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;
state.dimensions.width = bboxDims.width;
state.dimensions.height = bboxDims.height;
}
},
paramsReset: (state) => resetState(state),
Expand Down Expand Up @@ -488,7 +490,18 @@ export const paramsSliceConfig: SliceConfig<typeof slice> = {
schema: zParamsState,
getInitialState: getInitialParamsState,
persistConfig: {
migrate: (state) => zParamsState.parse(state),
migrate: (state) => {
assert(isPlainObject(state));

if (!('_version' in state)) {
// v0 -> v1, add _version and remove x/y from dimensions, lifting width/height to top level
state._version = 1;
state.dimensions.width = state.dimensions.rect.width;
state.dimensions.height = state.dimensions.rect.height;
}

return zParamsState.parse(state);
},
},
};

Expand Down Expand Up @@ -600,8 +613,8 @@ export const selectRefinerScheduler = createParamsSelector((params) => params.re
export const selectRefinerStart = createParamsSelector((params) => params.refinerStart);
export const selectRefinerSteps = createParamsSelector((params) => params.refinerSteps);

export const selectWidth = createParamsSelector((params) => params.dimensions.rect.width);
export const selectHeight = createParamsSelector((params) => params.dimensions.rect.height);
export const selectWidth = createParamsSelector((params) => params.dimensions.width);
export const selectHeight = createParamsSelector((params) => params.dimensions.height);
export const selectAspectRatioID = createParamsSelector((params) => params.dimensions.aspectRatio.id);
export const selectAspectRatioValue = createParamsSelector((params) => params.dimensions.aspectRatio.value);
export const selectAspectRatioIsLocked = createParamsSelector((params) => params.dimensions.aspectRatio.isLocked);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,18 +558,13 @@ const zBboxState = z.object({
});

const zDimensionsState = z.object({
// TODO(psyche): There is no concept of x/y coords for the dimensions state here... It's just width and height.
// Remove the extraneous data.
rect: z.object({
x: z.number().int(),
y: z.number().int(),
width: zParameterImageDimension,
height: zParameterImageDimension,
}),
width: zParameterImageDimension,
height: zParameterImageDimension,
aspectRatio: zAspectRatioConfig,
});

export const zParamsState = z.object({
_version: z.literal(1),
maskBlur: z.number(),
maskBlurMethod: zParameterMaskBlurMethod,
canvasCoherenceMode: zParameterCanvasCoherenceMode,
Expand Down Expand Up @@ -617,6 +612,7 @@ export const zParamsState = z.object({
});
export type ParamsState = z.infer<typeof zParamsState>;
export const getInitialParamsState = (): ParamsState => ({
_version: 1,
maskBlur: 16,
maskBlurMethod: 'box',
canvasCoherenceMode: 'Gaussian Blur',
Expand Down Expand Up @@ -661,7 +657,8 @@ export const getInitialParamsState = (): ParamsState => ({
clipGEmbedModel: null,
controlLora: null,
dimensions: {
rect: { x: 0, y: 0, width: 512, height: 512 },
width: 512,
height: 512,
aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG),
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,7 @@ export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => {
const scaledSize = ['auto', 'manual'].includes(canvas.bbox.scaleMethod) ? canvas.bbox.scaledSize : originalSize;
return { originalSize, scaledSize, aspectRatio };
} else if (tab === 'generate') {
const { rect, aspectRatio } = params.dimensions;
const { width, height } = rect;
const { width, height, aspectRatio } = params.dimensions;
return {
originalSize: { width, height },
scaledSize: { width, height },
Expand Down