refactor(ui): move size state to regional

This commit is contained in:
psychedelicious
2024-04-25 20:08:21 +10:00
committed by Kent Keirsey
parent b6a45e53f1
commit cc4bef4859
16 changed files with 118 additions and 109 deletions

View File

@ -8,9 +8,10 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice'; import { loraRemoved } from 'features/lora/store/loraSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { heightChanged, modelChanged, vaeSelected, widthChanged } from 'features/parameters/store/generationSlice'; import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { heightChanged, widthChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice'; import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import type { Logger } from 'roarr'; import type { Logger } from 'roarr';
@ -69,16 +70,22 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
dispatch(modelChanged(defaultModelInList, currentModel)); dispatch(modelChanged(defaultModelInList, currentModel));
const optimalDimension = getOptimalDimension(defaultModelInList); const optimalDimension = getOptimalDimension(defaultModelInList);
if (getIsSizeOptimal(state.generation.width, state.generation.height, optimalDimension)) { if (
getIsSizeOptimal(
state.regionalPrompts.present.size.width,
state.regionalPrompts.present.size.height,
optimalDimension
)
) {
return; return;
} }
const { width, height } = calculateNewSize( const { width, height } = calculateNewSize(
state.generation.aspectRatio.value, state.regionalPrompts.present.size.aspectRatio.value,
optimalDimension * optimalDimension optimalDimension * optimalDimension
); );
dispatch(widthChanged(width)); dispatch(widthChanged({ width }));
dispatch(heightChanged(height)); dispatch(heightChanged({ height }));
return; return;
} }
} }

View File

@ -1,14 +1,12 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { setDefaultSettings } from 'features/parameters/store/actions'; import { setDefaultSettings } from 'features/parameters/store/actions';
import { import {
heightRecalled,
setCfgRescaleMultiplier, setCfgRescaleMultiplier,
setCfgScale, setCfgScale,
setScheduler, setScheduler,
setSteps, setSteps,
vaePrecisionChanged, vaePrecisionChanged,
vaeSelected, vaeSelected,
widthRecalled,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import { import {
isParameterCFGRescaleMultiplier, isParameterCFGRescaleMultiplier,
@ -20,6 +18,7 @@ import {
isParameterWidth, isParameterWidth,
zParameterVAEModel, zParameterVAEModel,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import { heightChanged, widthChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast'; import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
@ -100,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
if (width) { if (width) {
if (isParameterWidth(width)) { if (isParameterWidth(width)) {
dispatch(widthRecalled(width)); dispatch(widthChanged({ width, updateAspectRatio: true }));
} }
} }
if (height) { if (height) {
if (isParameterHeight(height)) { if (isParameterHeight(height)) {
dispatch(heightRecalled(height)); dispatch(heightChanged({ height, updateAspectRatio: true }));
} }
} }

View File

@ -15,7 +15,8 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { heightChanged, selectOptimalDimension, widthChanged } from 'features/parameters/store/generationSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { heightChanged, widthChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -99,8 +100,8 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
controlImage.width / controlImage.height, controlImage.width / controlImage.height,
optimalDimension * optimalDimension optimalDimension * optimalDimension
); );
dispatch(widthChanged(width)); dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged(height)); dispatch(heightChanged({ height, updateAspectRatio: true }));
} }
}, [controlImage, activeTabName, dispatch, optimalDimension]); }, [controlImage, activeTabName, dispatch, optimalDimension]);

View File

@ -16,7 +16,6 @@ import type {
} from 'features/metadata/types'; } from 'features/metadata/types';
import { modelSelected } from 'features/parameters/store/actions'; import { modelSelected } from 'features/parameters/store/actions';
import { import {
heightRecalled,
initialImageChanged, initialImageChanged,
setCfgRescaleMultiplier, setCfgRescaleMultiplier,
setCfgScale, setCfgScale,
@ -25,7 +24,6 @@ import {
setSeed, setSeed,
setSteps, setSteps,
vaeSelected, vaeSelected,
widthRecalled,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import type { import type {
ParameterCFGRescaleMultiplier, ParameterCFGRescaleMultiplier,
@ -50,10 +48,12 @@ import type {
ParameterWidth, ParameterWidth,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import { import {
heightChanged,
negativePrompt2Changed, negativePrompt2Changed,
negativePromptChanged, negativePromptChanged,
positivePrompt2Changed, positivePrompt2Changed,
positivePromptChanged, positivePromptChanged,
widthChanged,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { import {
refinerModelChanged, refinerModelChanged,
@ -103,11 +103,11 @@ const recallInitialImage: MetadataRecallFunc<ImageDTO> = async (imageDTO) => {
}; };
const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => { const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => {
getStore().dispatch(widthRecalled(width)); getStore().dispatch(widthChanged({ width, updateAspectRatio: true }));
}; };
const recallHeight: MetadataRecallFunc<ParameterHeight> = (height) => { const recallHeight: MetadataRecallFunc<ParameterHeight> = (height) => {
getStore().dispatch(heightRecalled(height)); getStore().dispatch(heightChanged({ height, updateAspectRatio: true }));
}; };
const recallSteps: MetadataRecallFunc<ParameterSteps> = (steps) => { const recallSteps: MetadataRecallFunc<ParameterSteps> = (steps) => {

View File

@ -110,10 +110,9 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void =
const { vae, seamlessXAxis, seamlessYAxis } = state.generation; const { vae, seamlessXAxis, seamlessYAxis } = state.generation;
const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf;
const { width, height } = state.regionalPrompts.present.size;
const isAutoVae = !vae; const isAutoVae = !vae;
const isSeamlessEnabled = seamlessXAxis || seamlessYAxis; const isSeamlessEnabled = seamlessXAxis || seamlessYAxis;
const width = state.generation.width;
const height = state.generation.height;
const optimalDimension = selectOptimalDimension(state); const optimalDimension = selectOptimalDimension(state);
const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height); const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height);

View File

@ -47,8 +47,6 @@ export const buildLinearImageToImageGraph = async (state: RootState): Promise<No
initialImage, initialImage,
img2imgStrength: strength, img2imgStrength: strength,
shouldFitToWidthHeight, shouldFitToWidthHeight,
width,
height,
clipSkip, clipSkip,
shouldUseCpuNoise, shouldUseCpuNoise,
vaePrecision, vaePrecision,
@ -56,6 +54,7 @@ export const buildLinearImageToImageGraph = async (state: RootState): Promise<No
seamlessYAxis, seamlessYAxis,
} = state.generation; } = state.generation;
const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer; const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer;
const { width, height } = state.regionalPrompts.present.size;
/** /**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the

View File

@ -47,8 +47,6 @@ export const buildLinearSDXLImageToImageGraph = async (state: RootState): Promis
steps, steps,
initialImage, initialImage,
shouldFitToWidthHeight, shouldFitToWidthHeight,
width,
height,
shouldUseCpuNoise, shouldUseCpuNoise,
vaePrecision, vaePrecision,
seamlessXAxis, seamlessXAxis,
@ -56,6 +54,7 @@ export const buildLinearSDXLImageToImageGraph = async (state: RootState): Promis
img2imgStrength: strength, img2imgStrength: strength,
} = state.generation; } = state.generation;
const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer; const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer;
const { width, height } = state.regionalPrompts.present.size;
const { refinerModel, refinerStart } = state.sdxl; const { refinerModel, refinerStart } = state.sdxl;

View File

@ -36,14 +36,13 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise
scheduler, scheduler,
seed, seed,
steps, steps,
width,
height,
shouldUseCpuNoise, shouldUseCpuNoise,
vaePrecision, vaePrecision,
seamlessXAxis, seamlessXAxis,
seamlessYAxis, seamlessYAxis,
} = state.generation; } = state.generation;
const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer; const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer;
const { width, height } = state.regionalPrompts.present.size;
const { refinerModel, refinerStart } = state.sdxl; const { refinerModel, refinerStart } = state.sdxl;

View File

@ -35,8 +35,6 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise<Non
cfgRescaleMultiplier: cfg_rescale_multiplier, cfgRescaleMultiplier: cfg_rescale_multiplier,
scheduler, scheduler,
steps, steps,
width,
height,
clipSkip, clipSkip,
shouldUseCpuNoise, shouldUseCpuNoise,
vaePrecision, vaePrecision,
@ -45,6 +43,7 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise<Non
seed, seed,
} = state.generation; } = state.generation;
const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer; const { positivePrompt, negativePrompt } = state.regionalPrompts.present.baseLayer;
const { width, height } = state.regionalPrompts.present.size;
const use_cpu = shouldUseCpuNoise; const use_cpu = shouldUseCpuNoise;

View File

@ -1,11 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; 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 { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
import type { import type {
ParameterCanvasCoherenceMode, ParameterCanvasCoherenceMode,
@ -16,7 +12,7 @@ import type {
ParameterScheduler, ParameterScheduler,
ParameterVAEModel, ParameterVAEModel,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { configChanged } from 'features/system/store/configSlice'; import { configChanged } from 'features/system/store/configSlice';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import type { RgbaColor } from 'react-colorful'; import type { RgbaColor } from 'react-colorful';
@ -28,7 +24,6 @@ const initialGenerationState: GenerationState = {
_version: 2, _version: 2,
cfgScale: 7.5, cfgScale: 7.5,
cfgRescaleMultiplier: 0, cfgRescaleMultiplier: 0,
height: 512,
img2imgStrength: 0.75, img2imgStrength: 0.75,
infillMethod: 'patchmatch', infillMethod: 'patchmatch',
iterations: 1, iterations: 1,
@ -42,7 +37,6 @@ const initialGenerationState: GenerationState = {
shouldFitToWidthHeight: true, shouldFitToWidthHeight: true,
shouldRandomizeSeed: true, shouldRandomizeSeed: true,
steps: 50, steps: 50,
width: 512,
model: null, model: null,
vae: null, vae: null,
vaePrecision: 'fp32', vaePrecision: 'fp32',
@ -51,7 +45,6 @@ const initialGenerationState: GenerationState = {
clipSkip: 0, clipSkip: 0,
shouldUseCpuNoise: true, shouldUseCpuNoise: true,
shouldShowAdvancedOptions: false, shouldShowAdvancedOptions: false,
aspectRatio: { ...initialAspectRatioState },
infillTileSize: 32, infillTileSize: 32,
infillPatchmatchDownscaleSize: 1, infillPatchmatchDownscaleSize: 1,
infillMosaicTileWidth: 64, infillMosaicTileWidth: 64,
@ -140,19 +133,6 @@ export const generationSlice = createSlice({
const { maxClip } = CLIP_SKIP_MAP[newModel.base]; const { maxClip } = CLIP_SKIP_MAP[newModel.base];
state.clipSkip = clamp(state.clipSkip, 0, maxClip); state.clipSkip = clamp(state.clipSkip, 0, maxClip);
} }
if (action.meta.previousModel?.base === newModel.base) {
// The base model hasn't changed, we don't need to optimize the size
return;
}
const optimalDimension = getOptimalDimension(newModel);
if (getIsSizeOptimal(state.width, state.height, optimalDimension)) {
return;
}
const { width, height } = calculateNewSize(state.aspectRatio.value, optimalDimension * optimalDimension);
state.width = width;
state.height = height;
}, },
prepare: (payload: ParameterModel | null, previousModel?: ParameterModel | null) => ({ prepare: (payload: ParameterModel | null, previousModel?: ParameterModel | null) => ({
payload, payload,
@ -174,27 +154,6 @@ export const generationSlice = createSlice({
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => { shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
state.shouldUseCpuNoise = action.payload; state.shouldUseCpuNoise = action.payload;
}, },
widthChanged: (state, action: PayloadAction<number>) => {
state.width = action.payload;
},
heightChanged: (state, action: PayloadAction<number>) => {
state.height = action.payload;
},
widthRecalled: (state, action: PayloadAction<number>) => {
state.width = action.payload;
state.aspectRatio.value = action.payload / state.height;
state.aspectRatio.id = 'Free';
state.aspectRatio.isLocked = false;
},
heightRecalled: (state, action: PayloadAction<number>) => {
state.height = action.payload;
state.aspectRatio.value = state.width / action.payload;
state.aspectRatio.id = 'Free';
state.aspectRatio.isLocked = false;
},
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
state.aspectRatio = action.payload;
},
setInfillMethod: (state, action: PayloadAction<string>) => { setInfillMethod: (state, action: PayloadAction<string>) => {
state.infillMethod = action.payload; state.infillMethod = action.payload;
}, },
@ -229,15 +188,6 @@ export const generationSlice = createSlice({
state.vaePrecision = action.payload.sd.vaePrecision; state.vaePrecision = action.payload.sd.vaePrecision;
} }
}); });
// TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling
// factor than the UNet. Hopefully we get an upstream fix in diffusers.
builder.addMatcher(isAnyControlAdapterAdded, (state, action) => {
if (action.payload.type === 't2i_adapter') {
state.width = roundToMultiple(state.width, 64);
state.height = roundToMultiple(state.height, 64);
}
});
}, },
selectors: { selectors: {
selectOptimalDimension: (slice) => getOptimalDimension(slice.model), selectOptimalDimension: (slice) => getOptimalDimension(slice.model),
@ -268,11 +218,6 @@ export const {
setClipSkip, setClipSkip,
shouldUseCpuNoiseChanged, shouldUseCpuNoiseChanged,
vaePrecisionChanged, vaePrecisionChanged,
aspectRatioChanged,
widthChanged,
heightChanged,
widthRecalled,
heightRecalled,
setInfillTileSize, setInfillTileSize,
setInfillPatchmatchDownscaleSize, setInfillPatchmatchDownscaleSize,
setInfillMosaicTileWidth, setInfillMosaicTileWidth,

View File

@ -1,10 +1,8 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type { import type {
ParameterCanvasCoherenceMode, ParameterCanvasCoherenceMode,
ParameterCFGRescaleMultiplier, ParameterCFGRescaleMultiplier,
ParameterCFGScale, ParameterCFGScale,
ParameterHeight,
ParameterMaskBlurMethod, ParameterMaskBlurMethod,
ParameterModel, ParameterModel,
ParameterPrecision, ParameterPrecision,
@ -13,7 +11,6 @@ import type {
ParameterSteps, ParameterSteps,
ParameterStrength, ParameterStrength,
ParameterVAEModel, ParameterVAEModel,
ParameterWidth,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import type { RgbaColor } from 'react-colorful'; import type { RgbaColor } from 'react-colorful';
@ -21,7 +18,6 @@ export interface GenerationState {
_version: 2; _version: 2;
cfgScale: ParameterCFGScale; cfgScale: ParameterCFGScale;
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier; cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
height: ParameterHeight;
img2imgStrength: ParameterStrength; img2imgStrength: ParameterStrength;
infillMethod: string; infillMethod: string;
initialImage?: { imageName: string; width: number; height: number }; initialImage?: { imageName: string; width: number; height: number };
@ -36,7 +32,6 @@ export interface GenerationState {
shouldFitToWidthHeight: boolean; shouldFitToWidthHeight: boolean;
shouldRandomizeSeed: boolean; shouldRandomizeSeed: boolean;
steps: ParameterSteps; steps: ParameterSteps;
width: ParameterWidth;
model: ParameterModel | null; model: ParameterModel | null;
vae: ParameterVAEModel | null; vae: ParameterVAEModel | null;
vaePrecision: ParameterPrecision; vaePrecision: ParameterPrecision;
@ -45,7 +40,6 @@ export interface GenerationState {
clipSkip: number; clipSkip: number;
shouldUseCpuNoise: boolean; shouldUseCpuNoise: boolean;
shouldShowAdvancedOptions: boolean; shouldShowAdvancedOptions: boolean;
aspectRatio: AspectRatioState;
infillTileSize: number; infillTileSize: number;
infillPatchmatchDownscaleSize: number; infillPatchmatchDownscaleSize: number;
infillMosaicTileWidth: number; infillMosaicTileWidth: number;

View File

@ -43,8 +43,6 @@ const useStageRenderer = (
asPreview: boolean asPreview: boolean
) => { ) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const width = useAppSelector((s) => s.generation.width);
const height = useAppSelector((s) => s.generation.height);
const state = useAppSelector((s) => s.regionalPrompts.present); const state = useAppSelector((s) => s.regionalPrompts.present);
const tool = useStore($tool); const tool = useStore($tool);
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents(); const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents();
@ -121,11 +119,11 @@ const useStageRenderer = (
const stage = stageRef.current; const stage = stageRef.current;
const fitStageToContainer = () => { const fitStageToContainer = () => {
const newXScale = wrapper.offsetWidth / width; const newXScale = wrapper.offsetWidth / state.size.width;
const newYScale = wrapper.offsetHeight / height; const newYScale = wrapper.offsetHeight / state.size.height;
const newScale = Math.min(newXScale, newYScale, 1); const newScale = Math.min(newXScale, newYScale, 1);
stage.width(width * newScale); stage.width(state.size.width * newScale);
stage.height(height * newScale); stage.height(state.size.height * newScale);
stage.scaleX(newScale); stage.scaleX(newScale);
stage.scaleY(newScale); stage.scaleY(newScale);
}; };
@ -137,7 +135,7 @@ const useStageRenderer = (
return () => { return () => {
resizeObserver.disconnect(); resizeObserver.disconnect();
}; };
}, [stageRef, width, height, wrapper]); }, [stageRef, state.size.width, state.size.height, wrapper]);
useLayoutEffect(() => { useLayoutEffect(() => {
log.trace('Rendering tool preview'); log.trace('Rendering tool preview');
@ -188,8 +186,8 @@ const useStageRenderer = (
// The preview should not have a background // The preview should not have a background
return; return;
} }
renderers.renderBackground(stageRef.current, width, height); renderers.renderBackground(stageRef.current, state.size.width, state.size.height);
}, [stageRef, asPreview, width, height, renderers]); }, [stageRef, asPreview, state.size.width, state.size.height, renderers]);
useLayoutEffect(() => { useLayoutEffect(() => {
log.trace('Arranging layers'); log.trace('Arranging layers');

View File

@ -2,15 +2,24 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
import { controlAdapterRemoved } from 'features/controlAdapters/store/controlAdaptersSlice'; import { deepClone } from 'common/util/deepClone';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { controlAdapterRemoved, isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdapterConfig } from 'features/controlAdapters/store/types'; import type { ControlAdapterConfig } from 'features/controlAdapters/store/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { modelChanged } from 'features/parameters/store/generationSlice';
import type { import type {
ParameterAutoNegative, ParameterAutoNegative,
ParameterHeight,
ParameterNegativePrompt, ParameterNegativePrompt,
ParameterNegativeStylePromptSDXL, ParameterNegativeStylePromptSDXL,
ParameterPositivePrompt, ParameterPositivePrompt,
ParameterPositiveStylePromptSDXL, ParameterPositiveStylePromptSDXL,
ParameterWidth,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
@ -86,6 +95,11 @@ type RegionalPromptsState = {
globalMaskLayerOpacity: number; globalMaskLayerOpacity: number;
isEnabled: boolean; isEnabled: boolean;
baseLayer: BaseLayerState; baseLayer: BaseLayerState;
size: {
width: ParameterWidth;
height: ParameterHeight;
aspectRatio: AspectRatioState;
};
}; };
export const initialRegionalPromptsState: RegionalPromptsState = { export const initialRegionalPromptsState: RegionalPromptsState = {
@ -102,6 +116,11 @@ export const initialRegionalPromptsState: RegionalPromptsState = {
negativePrompt2: '', negativePrompt2: '',
shouldConcatPrompts: true, shouldConcatPrompts: true,
}, },
size: {
width: 512,
height: 512,
aspectRatio: deepClone(initialAspectRatioState),
},
}; };
const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line'; const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line';
@ -364,6 +383,27 @@ export const regionalPromptsSlice = createSlice({
shouldConcatPromptsChanged: (state, action: PayloadAction<boolean>) => { shouldConcatPromptsChanged: (state, action: PayloadAction<boolean>) => {
state.baseLayer.shouldConcatPrompts = action.payload; state.baseLayer.shouldConcatPrompts = action.payload;
}, },
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean }>) => {
const { width, updateAspectRatio } = action.payload;
state.size.width = width;
if (updateAspectRatio) {
state.size.aspectRatio.value = width / state.size.height;
state.size.aspectRatio.id = 'Free';
state.size.aspectRatio.isLocked = false;
}
},
heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean }>) => {
const { height, updateAspectRatio } = action.payload;
state.size.height = height;
if (updateAspectRatio) {
state.size.aspectRatio.value = state.size.width / height;
state.size.aspectRatio.id = 'Free';
state.size.aspectRatio.isLocked = false;
}
},
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
state.size.aspectRatio = action.payload;
},
//#endregion //#endregion
//#region General //#region General
@ -396,6 +436,30 @@ export const regionalPromptsSlice = createSlice({
layer.ipAdapterIds = layer.ipAdapterIds.filter((id) => id !== action.payload.id); layer.ipAdapterIds = layer.ipAdapterIds.filter((id) => id !== action.payload.id);
}); });
}); });
builder.addCase(modelChanged, (state, action) => {
const newModel = action.payload;
if (!newModel || action.meta.previousModel?.base === newModel.base) {
// Model was cleared or the base didn't change
return;
}
const optimalDimension = getOptimalDimension(newModel);
if (getIsSizeOptimal(state.size.width, state.size.height, optimalDimension)) {
return;
}
const { width, height } = calculateNewSize(state.size.aspectRatio.value, optimalDimension * optimalDimension);
state.size.width = width;
state.size.height = height;
});
// TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling
// factor than the UNet. Hopefully we get an upstream fix in diffusers.
builder.addMatcher(isAnyControlAdapterAdded, (state, action) => {
if (action.payload.type === 't2i_adapter') {
state.size.width = roundToMultiple(state.size.width, 64);
state.size.height = roundToMultiple(state.size.height, 64);
}
});
}, },
}); });
@ -461,6 +525,9 @@ export const {
positivePrompt2Changed, positivePrompt2Changed,
negativePrompt2Changed, negativePrompt2Changed,
shouldConcatPromptsChanged, shouldConcatPromptsChanged,
widthChanged,
heightChanged,
aspectRatioChanged,
// General actions // General actions
brushSizeChanged, brushSizeChanged,
globalMaskLayerOpacityChanged, globalMaskLayerOpacityChanged,

View File

@ -17,9 +17,11 @@ export const getRegionalPromptLayerBlobs = async (
preview: boolean = false preview: boolean = false
): Promise<Record<string, Blob>> => { ): Promise<Record<string, Blob>> => {
const state = getStore().getState(); const state = getStore().getState();
const reduxLayers = state.regionalPrompts.present.layers.filter(isVectorMaskLayer); const { layers } = state.regionalPrompts.present;
const { width, height } = state.regionalPrompts.present.size;
const reduxLayers = layers.filter(isVectorMaskLayer);
const container = document.createElement('div'); const container = document.createElement('div');
const stage = new Konva.Stage({ container, width: state.generation.width, height: state.generation.height }); const stage = new Konva.Stage({ container, width, height });
renderers.renderLayers(stage, reduxLayers, 1, 'brush'); renderers.renderLayers(stage, reduxLayers, 1, 'brush');
const konvaLayers = stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`); const konvaLayers = stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`);

View File

@ -14,6 +14,7 @@ import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamS
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
@ -24,8 +25,8 @@ import { ImageSizeCanvas } from './ImageSizeCanvas';
import { ImageSizeLinear } from './ImageSizeLinear'; import { ImageSizeLinear } from './ImageSizeLinear';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
[selectGenerationSlice, selectCanvasSlice, selectHrfSlice, activeTabNameSelector], [selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectRegionalPromptsSlice, activeTabNameSelector],
(generation, canvas, hrf, activeTabName) => { (generation, canvas, hrf, regionalPrompts, activeTabName) => {
const { shouldRandomizeSeed, model } = generation; const { shouldRandomizeSeed, model } = generation;
const { hrfEnabled } = hrf; const { hrfEnabled } = hrf;
const badges: string[] = []; const badges: string[] = [];
@ -42,7 +43,7 @@ const selector = createMemoizedSelector(
badges.push('locked'); badges.push('locked');
} }
} else { } else {
const { aspectRatio, width, height } = generation; const { aspectRatio, width, height } = regionalPrompts.present.size;
badges.push(`${width}×${height}`); badges.push(`${width}×${height}`);
badges.push(aspectRatio.id); badges.push(aspectRatio.id);
if (aspectRatio.isLocked) { if (aspectRatio.isLocked) {

View File

@ -5,27 +5,27 @@ import { AspectRatioCanvasPreview } from 'features/parameters/components/ImageSi
import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview'; import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview';
import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize'; import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { aspectRatioChanged, heightChanged, widthChanged } from 'features/parameters/store/generationSlice'; import { aspectRatioChanged, heightChanged, widthChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
export const ImageSizeLinear = memo(() => { export const ImageSizeLinear = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const tab = useAppSelector(activeTabNameSelector); const tab = useAppSelector(activeTabNameSelector);
const width = useAppSelector((s) => s.generation.width); const width = useAppSelector((s) => s.regionalPrompts.present.size.width);
const height = useAppSelector((s) => s.generation.height); const height = useAppSelector((s) => s.regionalPrompts.present.size.height);
const aspectRatioState = useAppSelector((s) => s.generation.aspectRatio); const aspectRatioState = useAppSelector((s) => s.regionalPrompts.present.size.aspectRatio);
const onChangeWidth = useCallback( const onChangeWidth = useCallback(
(width: number) => { (width: number) => {
dispatch(widthChanged(width)); dispatch(widthChanged({ width }));
}, },
[dispatch] [dispatch]
); );
const onChangeHeight = useCallback( const onChangeHeight = useCallback(
(height: number) => { (height: number) => {
dispatch(heightChanged(height)); dispatch(heightChanged({ height }));
}, },
[dispatch] [dispatch]
); );