From 4ee248b73652ac9f44f9c361152be6d2c6224b47 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:47:27 +1000 Subject: [PATCH] feat(ui): handle FLUX bbox constraints - Update canvas slice's to track the current base model architecture instead of just the optimal dimension. This lets us derive both optimal dimension _and_ grid size for the currently selected model. - Update all bbox size utilities to use derived grid size instead of hardcoded values of 8 or 64 - Review every damned instance of the number 8 in the whole frontend and update the ones that need to use the grid size - Update the invoke button blocking logic to check against scaled bbox size, unless scaling is disabled. - Update the invoke button blocking to say if it's width or height that is invalid and if its bbox or scaled, for both FLUX and the T2I adapter constraints --- invokeai/frontend/web/public/locales/en.json | 12 +- .../listeners/modelSelected.ts | 5 +- .../listeners/modelsLoaded.ts | 6 - .../src/common/hooks/useIsReadyToEnqueue.ts | 64 ++++++++- .../controlLayers/konva/CanvasBboxModule.ts | 14 +- .../konva/CanvasStateApiModule.ts | 18 ++- .../controlLayers/store/canvasSlice.ts | 127 ++++++++++++------ .../features/controlLayers/store/selectors.ts | 17 ++- .../src/features/controlLayers/store/types.ts | 4 +- .../util/getScaledBoundingBoxDimensions.ts | 35 ++++- .../MainModelDefaultSettings.tsx | 5 +- .../web/src/features/nodes/types/common.ts | 3 + .../parameters/components/Bbox/BboxHeight.tsx | 7 +- .../components/Bbox/BboxScaledHeight.tsx | 7 +- .../components/Bbox/BboxScaledWidth.tsx | 8 +- .../parameters/components/Bbox/BboxWidth.tsx | 7 +- .../components/Bbox/calculateNewSize.ts | 19 --- .../parameters/util/optimalDimension.ts | 48 ++++++- .../UpscaleSettingsAccordion.tsx | 7 +- 19 files changed, 297 insertions(+), 116 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 031c71aa8f..d9d001fc76 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1006,7 +1006,11 @@ "noFLUXVAEModelSelected": "No VAE model selected for FLUX generation", "noCLIPEmbedModelSelected": "No CLIP Embed model selected for FLUX generation", "canvasManagerNotLoaded": "Canvas Manager not loaded", - "fluxModelIncompatibleDimensions": "FLUX requires image dimension to be multiples of 16", + "fluxRequiresDimensionsToBeMultipleOf16": "FLUX requires width/height to be multiple of 16", + "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox width is {{width}}", + "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox height is {{height}}", + "fluxModelIncompatibleScaledWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox width is {{width}}", + "fluxModelIncompatibleScaledHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox height is {{height}}", "canvasIsFiltering": "Canvas is filtering", "canvasIsTransforming": "Canvas is transforming", "canvasIsRasterizing": "Canvas is rasterizing", @@ -1020,7 +1024,11 @@ "controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model", "controlAdapterNoImageSelected": "no Control Adapter image selected", "controlAdapterImageNotProcessed": "Control Adapter image not processed", - "t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of {{multiple}}", + "t2iAdapterRequiresDimensionsToBeMultipleOf": "T2I Adapter requires width/height to be multiple of", + "t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, bbox width is {{width}}", + "t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, bbox height is {{height}}", + "t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, scaled bbox width is {{width}}", + "t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, scaled bbox height is {{height}}", "ipAdapterNoModelSelected": "no IP adapter selected", "ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model", "ipAdapterNoImageSelected": "no IP Adapter image selected", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 39a6494d20..6d05c8dbf8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,12 +1,11 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { bboxOptimalDimensionChanged, bboxSyncedToOptimalDimension } from 'features/controlLayers/store/canvasSlice'; +import { bboxSyncedToOptimalDimension } from 'features/controlLayers/store/canvasSlice'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraDeleted } from 'features/controlLayers/store/lorasSlice'; import { modelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { modelSelected } from 'features/parameters/store/actions'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -71,8 +70,6 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } dispatch(modelChanged({ model: newModel, previousModel: state.params.model })); - // When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync. - dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(newModel) })); if (!selectIsStaging(state)) { dispatch(bboxSyncedToOptimalDimension()); } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 8ca78373e9..5d7ffbfbe1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -3,7 +3,6 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import type { AppDispatch, RootState } from 'app/store/store'; import type { SerializableObject } from 'common/types'; import { - bboxOptimalDimensionChanged, bboxSyncedToOptimalDimension, controlLayerModelChanged, referenceImageIPAdapterModelChanged, @@ -29,7 +28,6 @@ import { zParameterT5EncoderModel, zParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { Logger } from 'roarr'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; @@ -123,8 +121,6 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { 'No selected main model or selected main model is not available, selecting default model' ); dispatch(modelChanged({ model: zParameterModel.parse(defaultModel), previousModel: selectedMainModel })); - // When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync. - dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(defaultModel) })); if (!selectIsStaging(state)) { dispatch(bboxSyncedToOptimalDimension()); } @@ -137,8 +133,6 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { 'No selected main model or selected main model is not available, selecting first available model' ); dispatch(modelChanged({ model: zParameterModel.parse(firstModel), previousModel: selectedMainModel })); - // When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync. - dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(firstModel) })); if (!selectIsStaging(state)) { dispatch(bboxSyncedToOptimalDimension()); } diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index b74331cead..7d2c19db55 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -157,8 +157,32 @@ const createSelector = ( if (!params.fluxVAE) { reasons.push({ content: i18n.t('parameters.invoke.noFLUXVAEModelSelected') }); } - if (bbox.rect.width % 16 !== 0 || bbox.rect.height % 16 !== 0) { - reasons.push({ content: i18n.t('parameters.invoke.fluxModelIncompatibleDimensions') }); + if (bbox.scaleMethod === 'none') { + if (bbox.rect.width % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.fluxModelIncompatibleBboxWidth', { width: bbox.rect.width }), + }); + } + if (bbox.rect.height % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.fluxModelIncompatibleBboxHeight', { height: bbox.rect.height }), + }); + } + } else { + if (bbox.scaledSize.width % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.fluxModelIncompatibleScaledBboxWidth', { + width: bbox.scaledSize.width, + }), + }); + } + if (bbox.scaledSize.height % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.fluxModelIncompatibleScaledBboxHeight', { + height: bbox.scaledSize.height, + }), + }); + } } } @@ -181,8 +205,40 @@ const createSelector = ( // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) if (controlLayer.controlAdapter.type === 't2i_adapter') { const multiple = model?.base === 'sdxl' ? 32 : 64; - if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) { - problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); + if (bbox.scaleMethod === 'none') { + if (bbox.rect.width % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxWidth', { + multiple, + width: bbox.rect.width, + }), + }); + } + if (bbox.rect.height % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxHeight', { + multiple, + height: bbox.rect.height, + }), + }); + } + } else { + if (bbox.scaledSize.width % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxWidth', { + multiple, + width: bbox.scaledSize.width, + }), + }); + } + if (bbox.scaledSize.height % 16 !== 0) { + reasons.push({ + content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxHeight', { + multiple, + height: bbox.scaledSize.height, + }), + }); + } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts index 931ccf2c9e..1c6a2c8014 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts @@ -241,6 +241,8 @@ export class CanvasBboxModule extends CanvasModuleBase { * - Pushes the new bbox rect into app state */ onDragMove = () => { + // The grid size here is the _position_ grid size, not the _dimension_ grid size - it is not constratined by the + // currently-selected model. const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64; const bbox = this.manager.stateApi.getBbox(); const bboxRect: Rect = { @@ -277,7 +279,7 @@ export class CanvasBboxModule extends CanvasModuleBase { const shift = this.manager.stateApi.$shiftKey.get(); // Grid size depends on the modifier keys - let gridSize = ctrl || meta ? 8 : 64; + let gridSize = ctrl || meta ? this.manager.stateApi.getBboxGridSize() : 64; // Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the // new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if @@ -384,7 +386,7 @@ export class CanvasBboxModule extends CanvasModuleBase { // Determine the bbox size that fits within the visible rect. The bbox must be at least 64px in width and height, // and its width and height must be multiples of 8px. - const gridSize = 8; + const gridSize = this.manager.stateApi.getBboxGridSize(); // To be conservative, we will round up the x and y to the nearest grid size, and round down the width and height. // This ensures the bbox is never _larger_ than the visible rect. If the bbox is larger than the visible, we @@ -407,8 +409,12 @@ export class CanvasBboxModule extends CanvasModuleBase { const stage = this.konva.transformer.getStage(); assert(stage, 'Stage must exist'); - // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid. - const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64; + // We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finest grid size allowed + // currently-selected model. + const gridSize = + this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() + ? this.manager.stateApi.getBboxGridSize() + : 64; // Because we are working in absolute coordinates, we need to scale the grid size by the stage scale. const scaledGridSize = gridSize * stage.scaleX(); // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 7bfbbe9dd4..9dd2308aed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -25,7 +25,12 @@ import { entityReset, } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasStagingAreaSlice } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { selectAllRenderableEntities, selectBbox, selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { + selectAllRenderableEntities, + selectBbox, + selectCanvasSlice, + selectGridSize, +} from 'features/controlLayers/store/selectors'; import type { CanvasEntityType, CanvasState, @@ -401,6 +406,10 @@ export class CanvasStateApiModule extends CanvasModuleBase { return this.runSelector(selectCanvasSettingsSlice); }; + /** + * Gets the _positional_ grid size for the current canvas. Note that this is not the same as bbox grid size, which is + * based on the currently-selected model. + */ getGridSize = (): number => { const snapToGrid = this.getSettings().snapToGrid; if (!snapToGrid) { @@ -448,6 +457,13 @@ export class CanvasStateApiModule extends CanvasModuleBase { return this.runSelector(selectCanvasStagingAreaSlice); }; + /** + * Gets the grid size for the current canvas, based on the currently-selected model + */ + getBboxGridSize = (): number => { + return this.runSelector(selectGridSize); + }; + /** * Checks if an entity is selected. */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 00e150b3df..d62c17f786 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -6,6 +6,7 @@ import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; +import { modelChanged } from 'features/controlLayers/store/paramsSlice'; import { selectAllEntities, selectAllEntitiesOfType, @@ -19,12 +20,15 @@ import type { RegionalGuidanceReferenceImageState, RgbColor, } from 'features/controlLayers/store/types'; -import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; +import { + calculateNewSize, + getScaledBoundingBoxDimensions, +} from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize'; +import type { MainModelBase } from 'features/nodes/types/common'; +import { isMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; import { ASPECT_RATIO_MAP } from 'features/parameters/components/Bbox/constants'; -import { getIsSizeOptimal } from 'features/parameters/util/optimalDimension'; +import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; import { merge, omit } from 'lodash-es'; import type { UndoableOptions } from 'redux-undo'; @@ -89,7 +93,6 @@ const getInitialState = (): CanvasState => { referenceImages: { entities: [] }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, - optimalDimension: 512, aspectRatio: { id: '1:1', value: 1, @@ -100,6 +103,7 @@ const getInitialState = (): CanvasState => { width: 512, height: 512, }, + modelBase: 'sd-1', }, }; return initialState; @@ -629,15 +633,27 @@ export const canvasSlice = createSlice({ }, //#region BBox bboxScaledWidthChanged: (state, action: PayloadAction) => { - state.bbox.scaledSize.width = action.payload; + const gridSize = getGridSize(state.bbox.modelBase); + + state.bbox.scaledSize.width = roundToMultiple(action.payload, gridSize); + if (state.bbox.aspectRatio.isLocked) { - state.bbox.scaledSize.height = roundToMultiple(state.bbox.scaledSize.width / state.bbox.aspectRatio.value, 8); + state.bbox.scaledSize.height = roundToMultiple( + state.bbox.scaledSize.width / state.bbox.aspectRatio.value, + gridSize + ); } }, bboxScaledHeightChanged: (state, action: PayloadAction) => { - state.bbox.scaledSize.height = action.payload; + const gridSize = getGridSize(state.bbox.modelBase); + + state.bbox.scaledSize.height = roundToMultiple(action.payload, gridSize); + if (state.bbox.aspectRatio.isLocked) { - state.bbox.scaledSize.width = roundToMultiple(state.bbox.scaledSize.height * state.bbox.aspectRatio.value, 8); + state.bbox.scaledSize.width = roundToMultiple( + state.bbox.scaledSize.height * state.bbox.aspectRatio.value, + gridSize + ); } }, bboxScaleMethodChanged: (state, action: PayloadAction) => { @@ -660,10 +676,11 @@ export const canvasSlice = createSlice({ action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> ) => { const { width, updateAspectRatio, clamp } = action.payload; - state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; + const gridSize = getGridSize(state.bbox.modelBase); + state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; if (state.bbox.aspectRatio.isLocked) { - state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, 8); + state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, gridSize); } if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { @@ -679,11 +696,11 @@ export const canvasSlice = createSlice({ action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> ) => { const { height, updateAspectRatio, clamp } = action.payload; - - state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; + const gridSize = getGridSize(state.bbox.modelBase); + state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; if (state.bbox.aspectRatio.isLocked) { - state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, 8); + state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, gridSize); } if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { @@ -708,7 +725,8 @@ export const canvasSlice = createSlice({ state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; const { width, height } = calculateNewSize( state.bbox.aspectRatio.value, - state.bbox.rect.width * state.bbox.rect.height + state.bbox.rect.width * state.bbox.rect.height, + state.bbox.modelBase ); state.bbox.rect.width = width; state.bbox.rect.height = height; @@ -726,7 +744,8 @@ export const canvasSlice = createSlice({ } else { const { width, height } = calculateNewSize( state.bbox.aspectRatio.value, - state.bbox.rect.width * state.bbox.rect.height + state.bbox.rect.width * state.bbox.rect.height, + state.bbox.modelBase ); state.bbox.rect.width = width; state.bbox.rect.height = height; @@ -736,33 +755,37 @@ export const canvasSlice = createSlice({ syncScaledSize(state); }, bboxSizeOptimized: (state) => { + const optimalDimension = getOptimalDimension(state.bbox.modelBase); if (state.bbox.aspectRatio.isLocked) { - const { width, height } = calculateNewSize(state.bbox.aspectRatio.value, state.bbox.optimalDimension ** 2); + const { width, height } = calculateNewSize( + state.bbox.aspectRatio.value, + optimalDimension * optimalDimension, + state.bbox.modelBase + ); state.bbox.rect.width = width; state.bbox.rect.height = height; } else { state.bbox.aspectRatio = deepClone(initialState.bbox.aspectRatio); - state.bbox.rect.width = state.bbox.optimalDimension; - state.bbox.rect.height = state.bbox.optimalDimension; + state.bbox.rect.width = optimalDimension; + state.bbox.rect.height = optimalDimension; } syncScaledSize(state); }, - bboxOptimalDimensionChanged: (state, action: PayloadAction<{ optimalDimension: number }>) => { - // When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync. - // This action does the syncing. `bboxSyncedToOptimalDimension` below will actually change the bbox, - // and is only called when we are not staging. - const { optimalDimension } = action.payload; - state.bbox.optimalDimension = optimalDimension; - - // But! We do want to update the _scaled_ size. This handles the case where the user changes the base model type - // during staging. Though the generation bbox must be unchanged, the scaled bbox should adapt to the model. + bboxModelBaseChanged: (state, action: PayloadAction<{ modelBase: MainModelBase }>) => { + const { modelBase } = action.payload; + state.bbox.modelBase = modelBase; syncScaledSize(state); }, bboxSyncedToOptimalDimension: (state) => { - const { optimalDimension } = state.bbox; - if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) { - const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension); + const optimalDimension = getOptimalDimension(state.bbox.modelBase); + + if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, state.bbox.modelBase)) { + const bboxDims = calculateNewSize( + state.bbox.aspectRatio.value, + optimalDimension * optimalDimension, + state.bbox.modelBase + ); state.bbox.rect.width = bboxDims.width; state.bbox.rect.height = bboxDims.height; syncScaledSize(state); @@ -1073,6 +1096,32 @@ export const canvasSlice = createSlice({ builder.addCase(canvasReset, (state) => { return resetState(state); }); + builder.addCase(modelChanged, (state, action) => { + const { model } = action.payload; + /** + * Because the bbox depends in part on the model, it needs to be in sync with the model. However, due to + * complications with managing undo/redo history, we need to store the model in a separate slice from the canvas + * state. + * + * Unfortunately, this means we need to manually sync the model with the canvas state. We only care about the + * model base, so we only need to update the bbox's modelBase field. + * + * When we do this, we also want to update the bbox's dimensions - but only if we are not staging images on the + * canvas, during which time the bbox must stay the same. + * + * Unfortunately (again), the staging state is in a different slice, to prevent issues with undo/redo history. + * + * There's some fanagling we must do to handle this correctly: + * - Store the model base in this slice, so that we can access it when the user changes the bbox dimensions. + * - Avoid updating the bbox dimensions when we are staging - only update the model base. + * - Provide a separate action that will update the bbox dimensions and be careful to not dispatch it when staging. + */ + const base = model?.base; + if (isMainModelBase(base) && state.bbox.modelBase !== base) { + state.bbox.modelBase = base; + syncScaledSize(state); + } + }); }, }); @@ -1081,10 +1130,12 @@ const resetState = (state: CanvasState) => { // We need to retain the optimal dimension across resets, as it is changed only when the model changes. Copy it // from the old state, then recalculate the bbox size & scaled size. - newState.bbox.optimalDimension = state.bbox.optimalDimension; + newState.bbox.modelBase = state.bbox.modelBase; + const optimalDimension = getOptimalDimension(newState.bbox.modelBase); const rect = calculateNewSize( newState.bbox.aspectRatio.value, - newState.bbox.optimalDimension * newState.bbox.optimalDimension + optimalDimension * optimalDimension, + newState.bbox.modelBase ); newState.bbox.rect.width = rect.width; newState.bbox.rect.height = rect.height; @@ -1132,7 +1183,6 @@ export const { bboxAspectRatioIdChanged, bboxDimensionsSwapped, bboxSizeOptimized, - bboxOptimalDimensionChanged, bboxSyncedToOptimalDimension, // Raster layers rasterLayerAdded, @@ -1191,13 +1241,13 @@ const syncScaledSize = (state: CanvasState) => { if (state.bbox.scaleMethod === 'auto') { // Sync both aspect ratio and size const { width, height } = state.bbox.rect; - state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.optimalDimension); + state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.modelBase); } else if (state.bbox.scaleMethod === 'manual' && state.bbox.aspectRatio.isLocked) { // Only sync the aspect ratio if manual & locked state.bbox.scaledSize = calculateNewSize( state.bbox.aspectRatio.value, state.bbox.scaledSize.width * state.bbox.scaledSize.height, - 64 + state.bbox.modelBase ); } }; @@ -1214,11 +1264,6 @@ export const canvasUndoableConfig: UndoableOptions = if (!action.type.startsWith(canvasSlice.name)) { return false; } - if (bboxOptimalDimensionChanged.match(action)) { - // This action is not triggered by the user. it's dispatched when the model is changed and will have no visible - // effect on the canvas. - return false; - } // Throttle rapid actions of the same type filter = actionsThrottlingFilter(action); return filter; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 165a8639ab..88513781e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -15,7 +15,7 @@ import type { CanvasState, } from 'features/controlLayers/store/types'; import { isRasterLayerEntityIdentifier } from 'features/controlLayers/store/types'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { assert } from 'tsafe'; /** @@ -102,10 +102,19 @@ export const selectEntityCountActive = createSelector( export const selectHasEntities = createSelector(selectEntityCountAll, (count) => count > 0); /** - * Selects the optimal dimension for the canvas based on the currently-model + * Selects the optimal dimension for the canvas based on the currently-selected model */ -export const selectOptimalDimension = createSelector(selectParamsSlice, (params) => { - return getOptimalDimension(params.model); +export const selectOptimalDimension = createSelector(selectParamsSlice, (params): number => { + const modelBase = params.model?.base; + return getOptimalDimension(modelBase ?? null); +}); + +/** + * Selects the grid size for the canvas based on the currently-selected model + */ +export const selectGridSize = createSelector(selectParamsSlice, (params): number => { + const modelBase = params.model?.base; + return getGridSize(modelBase ?? null); }); /** diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 007e356764..eb1a4b22f4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,6 +1,6 @@ import type { SerializableObject } from 'common/types'; import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers'; -import { zModelIdentifierField } from 'features/nodes/types/common'; +import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas'; import { zParameterImageDimension, @@ -325,7 +325,7 @@ const zCanvasState = z.object({ height: zParameterImageDimension, }), scaleMethod: zBoundingBoxScaleMethod, - optimalDimension: z.number().int().positive(), + modelBase: zMainModelBase, }), }); export type CanvasState = z.infer; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts index c783e38b91..ded82765c2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/getScaledBoundingBoxDimensions.ts @@ -1,24 +1,27 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple'; import type { Dimensions } from 'features/controlLayers/store/types'; +import type { MainModelBase } from 'features/nodes/types/common'; +import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension'; /** * Scales the bounding box dimensions to the optimal dimension. The optimal dimensions should be the trained dimension * for the model. For example, 1024 for SDXL or 512 for SD1.5. * @param dimensions The un-scaled bbox dimensions - * @param optimalDimension The optimal dimension to scale the bbox to + * @param modelBase The base model */ -export const getScaledBoundingBoxDimensions = ( - dimensions: Dimensions, - optimalDimension: number, - gridSize: number = 64 -): Dimensions => { - const { width, height } = dimensions; +export const getScaledBoundingBoxDimensions = (dimensions: Dimensions, modelBase: MainModelBase): Dimensions => { + const optimalDimension = getOptimalDimension(modelBase); + const gridSize = getGridSize(modelBase); + const width = roundToMultiple(dimensions.width, gridSize); + const height = roundToMultiple(dimensions.height, gridSize); const scaledDimensions = { width, height }; const targetArea = optimalDimension * optimalDimension; const aspectRatio = width / height; + let currentArea = width * height; let maxDimension = optimalDimension - gridSize; + while (currentArea < targetArea) { maxDimension += gridSize; if (width === height) { @@ -39,3 +42,21 @@ export const getScaledBoundingBoxDimensions = ( return scaledDimensions; }; + +/** + * Calculate the new width and height that will fit the given aspect ratio, retaining the input area + * @param ratio The aspect ratio to calculate the new size for + * @param area The input area + * @param modelBase The base model + * @returns The width and height that will fit the given aspect ratio, retaining the input area + */ +export const calculateNewSize = (ratio: number, area: number, modelBase: MainModelBase): Dimensions => { + const exactWidth = Math.sqrt(area * ratio); + const exactHeight = exactWidth / ratio; + const gridSize = getGridSize(modelBase); + + return { + width: roundToMultiple(exactWidth, gridSize), + height: roundToMultiple(exactHeight, gridSize), + }; +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx index 9de6233730..f00e05f1fb 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx @@ -47,7 +47,10 @@ export const MainModelDefaultSettings = memo(({ modelConfig }: Props) => { const { t } = useTranslation(); const defaultSettingsDefaults = useMainModelDefaultSettings(modelConfig); - const optimalDimension = useMemo(() => getOptimalDimension(modelConfig), [modelConfig]); + const optimalDimension = useMemo(() => { + const modelBase = modelConfig?.base; + return getOptimalDimension(modelBase ?? null); + }, [modelConfig]); const [updateModel, { isLoading: isLoadingUpdateModel }] = useUpdateModelMutation(); const { handleSubmit, control, formState, reset } = useForm({ diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index 7d8a0e1868..60438ebef6 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -62,6 +62,9 @@ export type SchedulerField = z.infer; // #region Model-related schemas const zBaseModel = z.enum(['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner', 'flux']); +export const zMainModelBase = z.enum(['sd-1', 'sd-2', 'sdxl', 'flux']); +export type MainModelBase = z.infer; +export const isMainModelBase = (base: unknown): base is MainModelBase => zMainModelBase.safeParse(base).success; const zModelType = z.enum([ 'main', 'vae', diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx index f0a89a8943..f91b2dd9e5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { bboxHeightChanged } from 'features/controlLayers/store/canvasSlice'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { selectHeight, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectGridSize, selectHeight, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { selectHeightConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,6 +15,7 @@ export const BboxHeight = memo(() => { const height = useAppSelector(selectHeight); const config = useAppSelector(selectHeightConfig); const isStaging = useAppSelector(selectIsStaging); + const gridSize = useAppSelector(selectGridSize); const onChange = useCallback( (v: number) => { @@ -40,7 +41,7 @@ export const BboxHeight = memo(() => { min={config.sliderMin} max={config.sliderMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} marks={marks} /> { min={config.numberInputMin} max={config.numberInputMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} /> ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx index 46bff30f7e..5d7b3b0dfc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx @@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { bboxScaledHeightChanged } from 'features/controlLayers/store/canvasSlice'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,6 +23,7 @@ const BboxScaledHeight = () => { const isManual = useAppSelector(selectIsManual); const scaledHeight = useAppSelector(selectScaledHeight); const config = useAppSelector(selectScaledBoundingBoxHeightConfig); + const gridSize = useAppSelector(selectGridSize); const onChange = useCallback( (height: number) => { @@ -38,7 +39,7 @@ const BboxScaledHeight = () => { min={config.sliderMin} max={config.sliderMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} value={scaledHeight} onChange={onChange} marks @@ -48,7 +49,7 @@ const BboxScaledHeight = () => { min={config.numberInputMin} max={config.numberInputMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} value={scaledHeight} onChange={onChange} defaultValue={optimalDimension} diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx index 8132131f5a..7fe14df20d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx @@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { bboxScaledWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectCanvasSlice, selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,6 +23,8 @@ const BboxScaledWidth = () => { const isManual = useAppSelector(selectIsManual); const scaledWidth = useAppSelector(selectScaledWidth); const config = useAppSelector(selectScaledBoundingBoxWidthConfig); + const gridSize = useAppSelector(selectGridSize); + const onChange = useCallback( (width: number) => { dispatch(bboxScaledWidthChanged(width)); @@ -37,7 +39,7 @@ const BboxScaledWidth = () => { min={config.sliderMin} max={config.sliderMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} value={scaledWidth} onChange={onChange} defaultValue={optimalDimension} @@ -47,7 +49,7 @@ const BboxScaledWidth = () => { min={config.numberInputMin} max={config.numberInputMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} value={scaledWidth} onChange={onChange} defaultValue={optimalDimension} diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx index 38211f0cfe..81dbc8ddcf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { selectOptimalDimension, selectWidth } from 'features/controlLayers/store/selectors'; +import { selectGridSize, selectOptimalDimension, selectWidth } from 'features/controlLayers/store/selectors'; import { selectWidthConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,6 +15,7 @@ export const BboxWidth = memo(() => { const optimalDimension = useAppSelector(selectOptimalDimension); const config = useAppSelector(selectWidthConfig); const isStaging = useAppSelector(selectIsStaging); + const gridSize = useAppSelector(selectGridSize); const onChange = useCallback( (v: number) => { @@ -40,7 +41,7 @@ export const BboxWidth = memo(() => { min={config.sliderMin} max={config.sliderMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} marks={marks} /> { min={config.numberInputMin} max={config.numberInputMax} step={config.coarseStep} - fineStep={config.fineStep} + fineStep={gridSize} /> ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/calculateNewSize.ts b/invokeai/frontend/web/src/features/parameters/components/Bbox/calculateNewSize.ts index 92a9253b19..139597f9cb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/calculateNewSize.ts +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/calculateNewSize.ts @@ -1,21 +1,2 @@ -import { roundToMultiple } from 'common/util/roundDownToMultiple'; -/** - * Calculate the new width and height that will fit the given aspect ratio, retaining the input area - * @param ratio The aspect ratio to calculate the new size for - * @param area The input area - * @returns The width and height that will fit the given aspect ratio, retaining the input area - */ -export const calculateNewSize = ( - ratio: number, - area: number, - gridSize: number = 8 -): { width: number; height: number } => { - const exactWidth = Math.sqrt(area * ratio); - const exactHeight = exactWidth / ratio; - return { - width: roundToMultiple(exactWidth, gridSize), - height: roundToMultiple(exactHeight, gridSize), - }; -}; diff --git a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts index b416316f58..23afdf30fc 100644 --- a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts +++ b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts @@ -1,12 +1,45 @@ -import type { ModelIdentifierField } from 'features/nodes/types/common'; +import type { MainModelBase } from 'features/nodes/types/common'; +import type { BaseModelType } from 'services/api/types'; /** - * Gets the optimal dimension for a givel model, based on the model's base_model - * @param model The model identifier - * @returns The optimal dimension for the model + * Gets the optimal dimension for a given base model: + * - sd-1, sd-2: 512 + * - sdxl, flux: 1024 + * - default: 1024 + * @param base The base model + * @returns The optimal dimension for the model, defaulting to 512 */ -export const getOptimalDimension = (model?: ModelIdentifierField | null): number => - model?.base === 'sdxl' || model?.base === 'flux' ? 1024 : 512; +export const getOptimalDimension = (base?: BaseModelType | null): number => { + switch (base) { + case 'sd-1': + case 'sd-2': + return 512; + case 'sdxl': + case 'flux': + default: + return 1024; + } +}; + +/** + * Gets the grid size for a given base model. For Flux, the grid size is 16, otherwise it is 8. + * - sd-1, sd-2, sdxl: 8 + * - flux: 16 + * - default: 8 + * @param base The base model + * @returns The grid size for the model, defaulting to 8 + */ +export const getGridSize = (base?: BaseModelType | null): number => { + switch (base) { + case 'flux': + return 16; + case 'sd-1': + case 'sd-2': + case 'sdxl': + default: + return 8; + } +}; const MIN_AREA_FACTOR = 0.8; const MAX_AREA_FACTOR = 1.2; @@ -37,6 +70,7 @@ export const getIsSizeTooLarge = (width: number, height: number, optimalDimensio * @param optimalDimension The optimal dimension * @returns Whether the current width and height needs to be resized to the optimal dimension */ -export const getIsSizeOptimal = (width: number, height: number, optimalDimension: number): boolean => { +export const getIsSizeOptimal = (width: number, height: number, modelBase: MainModelBase): boolean => { + const optimalDimension = getOptimalDimension(modelBase); return !getIsSizeTooSmall(width, height, optimalDimension) && !getIsSizeTooLarge(width, height, optimalDimension); }; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion.tsx index 78e7df93fb..34b69e931f 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion.tsx @@ -1,10 +1,12 @@ import { Expander, Flex, StandaloneAccordion } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import ParamCreativity from 'features/parameters/components/Upscale/ParamCreativity'; import ParamSpandrelModel from 'features/parameters/components/Upscale/ParamSpandrelModel'; import ParamStructure from 'features/parameters/components/Upscale/ParamStructure'; import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; +import { getGridSize } from 'features/parameters/util/optimalDimension'; import { UpscaleScaleSlider } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; @@ -24,9 +26,10 @@ const selector = createMemoizedSelector([selectUpscaleSlice], (upscaleSlice) => } if (upscaleInitialImage) { + const gridSize = upscaleModel ? getGridSize(upscaleModel.base) : getGridSize(null); // Output height and width are scaled and rounded down to the nearest multiple of 8 - const outputWidth = Math.floor((upscaleInitialImage.width * scale) / 8) * 8; - const outputHeight = Math.floor((upscaleInitialImage.height * scale) / 8) * 8; + const outputWidth = roundDownToMultiple(upscaleInitialImage.width * scale, gridSize); + const outputHeight = roundDownToMultiple(upscaleInitialImage.height * scale, gridSize); badges.push(`${outputWidth}×${outputHeight}`); }