feat(ui): modularize imagesize components

Canvas and non-canvas have separate width and height and need their own separate aspect ratios. In order to not duplicate a lot of aspect ratio logic, the components relating to image size have been modularized.
This commit is contained in:
psychedelicious
2024-01-02 12:01:23 +11:00
committed by Kent Keirsey
parent 011757c497
commit 4f43eda09b
27 changed files with 614 additions and 554 deletions

View File

@@ -1,4 +1,4 @@
import type { GenerationState } from './generationSlice';
import type { GenerationState } from './types';
/**
* Generation slice persist denylist

View File

@@ -2,77 +2,25 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { ASPECT_RATIO_MAP } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioID } from 'features/parameters/components/ImageSize/types';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
import type {
ParameterCanvasCoherenceMode,
ParameterCFGRescaleMultiplier,
ParameterCFGScale,
ParameterHeight,
ParameterMaskBlurMethod,
ParameterModel,
ParameterNegativePrompt,
ParameterPositivePrompt,
ParameterPrecision,
ParameterScheduler,
ParameterSeed,
ParameterSteps,
ParameterStrength,
ParameterVAEModel,
ParameterWidth,
} from 'features/parameters/types/parameterSchemas';
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { configChanged } from 'features/system/store/configSlice';
import { clamp, cloneDeep } from 'lodash-es';
import { clamp } from 'lodash-es';
import type { ImageDTO } from 'services/api/types';
export interface GenerationState {
cfgScale: ParameterCFGScale;
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
height: ParameterHeight;
img2imgStrength: ParameterStrength;
infillMethod: string;
initialImage?: { imageName: string; width: number; height: number };
iterations: number;
perlin: number;
positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt;
scheduler: ParameterScheduler;
maskBlur: number;
maskBlurMethod: ParameterMaskBlurMethod;
canvasCoherenceMode: ParameterCanvasCoherenceMode;
canvasCoherenceSteps: number;
canvasCoherenceStrength: ParameterStrength;
seed: ParameterSeed;
seedWeights: string;
shouldFitToWidthHeight: boolean;
shouldGenerateVariations: boolean;
shouldRandomizeSeed: boolean;
steps: ParameterSteps;
threshold: number;
infillTileSize: number;
infillPatchmatchDownscaleSize: number;
variationAmount: number;
width: ParameterWidth;
shouldUseSymmetry: boolean;
horizontalSymmetrySteps: number;
verticalSymmetrySteps: number;
model: ParameterModel | null;
vae: ParameterVAEModel | null;
vaePrecision: ParameterPrecision;
seamlessXAxis: boolean;
seamlessYAxis: boolean;
clipSkip: number;
shouldUseCpuNoise: boolean;
shouldShowAdvancedOptions: boolean;
aspectRatio: {
id: AspectRatioID;
value: number;
isLocked: boolean;
};
}
import type { GenerationState } from './types';
export const initialGenerationState: GenerationState = {
cfgScale: 7.5,
@@ -112,18 +60,12 @@ export const initialGenerationState: GenerationState = {
clipSkip: 0,
shouldUseCpuNoise: true,
shouldShowAdvancedOptions: false,
aspectRatio: {
id: '1:1',
value: 1,
isLocked: false,
},
aspectRatio: { ...initialAspectRatioState },
};
const initialState: GenerationState = initialGenerationState;
export const generationSlice = createSlice({
name: 'generation',
initialState,
initialState: initialGenerationState,
reducers: {
setPositivePrompt: (state, action: PayloadAction<string>) => {
state.positivePrompt = action.payload;
@@ -279,93 +221,15 @@ export const generationSlice = createSlice({
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
state.shouldUseCpuNoise = action.payload;
},
aspectRatioSelected: (
state,
action: PayloadAction<GenerationState['aspectRatio']['id']>
) => {
const aspectRatioID = action.payload;
state.aspectRatio.id = aspectRatioID;
if (aspectRatioID === 'Free') {
// If the new aspect ratio is free, we only unlock
state.aspectRatio.isLocked = false;
} else {
// The new aspect ratio not free, so we need to coerce the size & lock
state.aspectRatio.isLocked = true;
const aspectRatio = ASPECT_RATIO_MAP[aspectRatioID].ratio;
state.aspectRatio.value = aspectRatio;
const { width, height } = calculateNewSize(
aspectRatio,
state.width,
state.height
);
state.width = width;
state.height = height;
}
},
dimensionsSwapped: (state) => {
// We always invert the aspect ratio
const aspectRatio = 1 / state.aspectRatio.value;
state.aspectRatio.value = aspectRatio;
if (state.aspectRatio.id === 'Free') {
// If the aspect ratio is free, we just swap the dimensions
const oldWidth = state.width;
const oldHeight = state.height;
state.width = oldHeight;
state.height = oldWidth;
} else {
// Else we need to calculate the new size
const { width, height } = calculateNewSize(
aspectRatio,
state.width,
state.height
);
state.width = width;
state.height = height;
// Update the aspect ratio ID to match the new aspect ratio
state.aspectRatio.id = ASPECT_RATIO_MAP[state.aspectRatio.id].inverseID;
}
},
widthChanged: (state, action: PayloadAction<GenerationState['width']>) => {
const width = action.payload;
let height = state.height;
if (state.aspectRatio.isLocked) {
// When locked, we calculate the new height based on the aspect ratio
height = roundToMultiple(width / state.aspectRatio.value, 8);
} else {
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
state.aspectRatio.isLocked = false;
state.aspectRatio.id = 'Free';
state.aspectRatio.value = width / height;
}
state.width = width;
state.height = height;
},
heightChanged: (
state,
action: PayloadAction<GenerationState['height']>
) => {
const height = action.payload;
let width = state.width;
if (state.aspectRatio.isLocked) {
// When locked, we calculate the new width based on the aspect ratio
width = roundToMultiple(height * state.aspectRatio.value, 8);
} else {
// Else we unlock, set the aspect ratio to free, and update the aspect ratio itself
state.aspectRatio.isLocked = false;
state.aspectRatio.id = 'Free';
state.aspectRatio.value = width / height;
}
state.height = height;
state.width = width;
},
isLockedToggled: (state) => {
state.aspectRatio.isLocked = !state.aspectRatio.isLocked;
},
sizeReset: (state, action: PayloadAction<number>) => {
state.aspectRatio = cloneDeep(initialGenerationState.aspectRatio);
widthChanged: (state, action: PayloadAction<number>) => {
state.width = action.payload;
},
heightChanged: (state, action: PayloadAction<number>) => {
state.height = action.payload;
},
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
state.aspectRatio = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(configChanged, (state, action) => {
@@ -436,13 +300,10 @@ export const {
setSeamlessYAxis,
setClipSkip,
shouldUseCpuNoiseChanged,
aspectRatioSelected,
dimensionsSwapped,
vaePrecisionChanged,
aspectRatioChanged,
widthChanged,
heightChanged,
isLockedToggled,
sizeReset,
vaePrecisionChanged,
} = generationSlice.actions;
export default generationSlice.reducer;

View File

@@ -0,0 +1,60 @@
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type {
ParameterCanvasCoherenceMode,
ParameterCFGRescaleMultiplier,
ParameterCFGScale,
ParameterHeight,
ParameterMaskBlurMethod,
ParameterModel,
ParameterNegativePrompt,
ParameterPositivePrompt,
ParameterPrecision,
ParameterScheduler,
ParameterSeed,
ParameterSteps,
ParameterStrength,
ParameterVAEModel,
ParameterWidth,
} from 'features/parameters/types/parameterSchemas';
export interface GenerationState {
cfgScale: ParameterCFGScale;
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
height: ParameterHeight;
img2imgStrength: ParameterStrength;
infillMethod: string;
initialImage?: { imageName: string; width: number; height: number };
iterations: number;
perlin: number;
positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt;
scheduler: ParameterScheduler;
maskBlur: number;
maskBlurMethod: ParameterMaskBlurMethod;
canvasCoherenceMode: ParameterCanvasCoherenceMode;
canvasCoherenceSteps: number;
canvasCoherenceStrength: ParameterStrength;
seed: ParameterSeed;
seedWeights: string;
shouldFitToWidthHeight: boolean;
shouldGenerateVariations: boolean;
shouldRandomizeSeed: boolean;
steps: ParameterSteps;
threshold: number;
infillTileSize: number;
infillPatchmatchDownscaleSize: number;
variationAmount: number;
width: ParameterWidth;
shouldUseSymmetry: boolean;
horizontalSymmetrySteps: number;
verticalSymmetrySteps: number;
model: ParameterModel | null;
vae: ParameterVAEModel | null;
vaePrecision: ParameterPrecision;
seamlessXAxis: boolean;
seamlessYAxis: boolean;
clipSkip: number;
shouldUseCpuNoise: boolean;
shouldShowAdvancedOptions: boolean;
aspectRatio: AspectRatioState;
}