diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 9c21fd40fe..3c09839078 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1079,6 +1079,11 @@ "aspect": "Aspect", "aspectRatio": "Aspect Ratio", "aspectRatioFree": "Free", + "lockAspectRatio": "Lock Aspect Ratio", + "swapDimensions": "Swap Dimensions", + "setToOptimalSize": "Optimize size for model", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (may be too small)", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (may be too large)", "boundingBoxHeader": "Bounding Box", "boundingBoxHeight": "Bounding Box Height", "boundingBoxWidth": "Bounding Box Width", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 452f599fd4..28bf04513b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -13,7 +13,10 @@ import type { import { imageSelected } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { workflowExposedFieldAdded } from 'features/nodes/store/workflowSlice'; -import { initialImageChanged } from 'features/parameters/store/generationSlice'; +import { + initialImageChanged, + selectOptimalDimension, +} from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '../'; @@ -26,7 +29,7 @@ export const dndDropped = createAction<{ export const addImageDroppedListener = () => { startAppListening({ actionCreator: dndDropped, - effect: async (action, { dispatch }) => { + effect: async (action, { dispatch, getState }) => { const log = logger('dnd'); const { activeData, overData } = action.payload; @@ -115,7 +118,12 @@ export const addImageDroppedListener = () => { activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - dispatch(setInitialCanvasImage(activeData.payload.imageDTO)); + dispatch( + setInitialCanvasImage( + activeData.payload.imageDTO, + selectOptimalDimension(getState()) + ) + ); return; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 196498a0f4..c7141c51c5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -6,7 +6,10 @@ import { controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { initialImageChanged } from 'features/parameters/store/generationSlice'; +import { + initialImageChanged, + selectOptimalDimension, +} from 'features/parameters/store/generationSlice'; import { addToast } from 'features/system/store/systemSlice'; import { t } from 'i18next'; import { omit } from 'lodash-es'; @@ -76,7 +79,9 @@ export const addImageUploadedFulfilledListener = () => { } if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') { - dispatch(setInitialCanvasImage(imageDTO)); + dispatch( + setInitialCanvasImage(imageDTO, selectOptimalDimension(state)) + ); dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, 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 dad75f9004..fe94953f23 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,5 +1,4 @@ import { logger } from 'app/logging/logger'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { controlAdapterIsEnabledChanged, selectControlAdapterAll, @@ -7,10 +6,8 @@ import { import { loraRemoved } from 'features/lora/store/loraSlice'; import { modelSelected } from 'features/parameters/store/actions'; import { - heightChanged, modelChanged, vaeSelected, - widthChanged, } from 'features/parameters/store/generationSlice'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { addToast } from 'features/system/store/systemSlice'; @@ -84,22 +81,6 @@ export const addModelSelectedListener = () => { } } - // Update Width / Height / Bounding Box Dimensions on Model Change - if ( - state.generation.model?.base_model !== newModel.base_model && - state.ui.shouldAutoChangeDimensions - ) { - if (['sdxl', 'sdxl-refiner'].includes(newModel.base_model)) { - dispatch(widthChanged(1024)); - dispatch(heightChanged(1024)); - dispatch(setBoundingBoxDimensions({ width: 1024, height: 1024 })); - } else { - dispatch(widthChanged(512)); - dispatch(heightChanged(512)); - dispatch(setBoundingBoxDimensions({ width: 512, height: 512 })); - } - } - dispatch(modelChanged(newModel)); }, }); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx index 3719fa5cd7..ae76bd346e 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx @@ -26,6 +26,8 @@ import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE, } from 'features/canvas/store/constants'; +import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import type Konva from 'konva'; import type { GroupConfig } from 'konva/lib/Group'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -37,8 +39,8 @@ import { Group, Rect, Transformer } from 'react-konva'; const borderDash = [4, 4]; const boundingBoxPreviewSelector = createMemoizedSelector( - [stateSelector], - ({ canvas }) => { + [stateSelector, selectOptimalDimension], + ({ canvas }, optimalDimension) => { const { boundingBoxCoordinates, boundingBoxDimensions, @@ -56,6 +58,7 @@ const boundingBoxPreviewSelector = createMemoizedSelector( tool, hitStrokeWidth: 20 / stageScale, aspectRatio, + optimalDimension, }; } ); @@ -73,6 +76,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { tool, hitStrokeWidth, aspectRatio, + optimalDimension, } = useAppSelector(boundingBoxPreviewSelector); const transformerRef = useRef(null); @@ -163,22 +167,25 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { const y = Math.round(rect.y()); if (aspectRatio.isLocked) { - const newHeight = roundDownToMultipleMin( - width / aspectRatio.value, - gridSize - ); + const newDimensions = calculateNewSize(aspectRatio.value, width * height); dispatch( - setBoundingBoxDimensions({ - width: roundDownToMultipleMin(width, gridSize), - height: newHeight, - }) + setBoundingBoxDimensions( + { + width: roundDownToMultipleMin(newDimensions.width, gridSize), + height: roundDownToMultipleMin(newDimensions.height, gridSize), + }, + optimalDimension + ) ); } else { dispatch( - setBoundingBoxDimensions({ - width: roundDownToMultipleMin(width, gridSize), - height: roundDownToMultipleMin(height, gridSize), - }) + setBoundingBoxDimensions( + { + width: roundDownToMultipleMin(width, gridSize), + height: roundDownToMultipleMin(height, gridSize), + }, + optimalDimension + ) ); dispatch( aspectRatioChanged({ @@ -205,6 +212,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { dispatch, shouldSnapToGrid, gridSize, + optimalDimension, ]); const anchorDragBoundFunc = useCallback( @@ -233,7 +241,6 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { x: roundDownToMultiple(newPos.x, scaledStep) + offsetX, y: roundDownToMultiple(newPos.y, scaledStep) + offsetY, }; - console.log({ oldPos, newPos, newCoordinates }); return newCoordinates; }, diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index f9b9b36b6b..afa0b3e08b 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -11,6 +11,12 @@ import floorCoordinates from 'features/canvas/util/floorCoordinates'; import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; import roundDimensionsToMultiple from 'features/canvas/util/roundDimensionsToMultiple'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; +import { modelChanged } from 'features/parameters/store/generationSlice'; +import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types'; +import { + getIsSizeOptimal, + getOptimalDimension, +} from 'features/parameters/util/optimalDimension'; import type { IRect, Vector2d } from 'konva/lib/types'; import { clamp, cloneDeep } from 'lodash-es'; import type { RgbaColor } from 'react-colorful'; @@ -86,6 +92,29 @@ export const initialCanvasState: CanvasState = { }, }; +const setBoundingBoxDimensionsReducer = ( + state: CanvasState, + payload: Partial, + optimalDimension: number +) => { + const boundingBoxDimensions = payload; + const newDimensions = roundDimensionsToMultiple( + { + ...state.boundingBoxDimensions, + ...boundingBoxDimensions, + }, + CANVAS_GRID_SIZE_FINE + ); + state.boundingBoxDimensions = newDimensions; + if (state.boundingBoxScaleMethod === 'auto') { + const scaledDimensions = getScaledBoundingBoxDimensions( + newDimensions, + optimalDimension + ); + state.scaledBoundingBoxDimensions = scaledDimensions; + } +}; + export const canvasSlice = createSlice({ name: 'canvas', initialState: initialCanvasState, @@ -132,117 +161,90 @@ export const canvasSlice = createSlice({ state.isMaskEnabled = action.payload; state.layer = action.payload ? 'mask' : 'base'; }, - setInitialCanvasImage: (state, action: PayloadAction) => { - const image = action.payload; - const { width, height } = image; - const { stageDimensions } = state; + setInitialCanvasImage: { + reducer: (state, action: PayloadActionWithOptimalDimension) => { + const { width, height, image_name } = action.payload; + const { optimalDimension } = action.meta; + const { stageDimensions } = state; - const newBoundingBoxDimensions = { - width: roundDownToMultiple( - clamp(width, CANVAS_GRID_SIZE_FINE, 512), - CANVAS_GRID_SIZE_FINE - ), - height: roundDownToMultiple( - clamp(height, CANVAS_GRID_SIZE_FINE, 512), - CANVAS_GRID_SIZE_FINE - ), - }; + const newBoundingBoxDimensions = { + width: roundDownToMultiple( + clamp(width, CANVAS_GRID_SIZE_FINE, optimalDimension), + CANVAS_GRID_SIZE_FINE + ), + height: roundDownToMultiple( + clamp(height, CANVAS_GRID_SIZE_FINE, optimalDimension), + CANVAS_GRID_SIZE_FINE + ), + }; - const newBoundingBoxCoordinates = { - x: roundToMultiple( - width / 2 - newBoundingBoxDimensions.width / 2, - CANVAS_GRID_SIZE_FINE - ), - y: roundToMultiple( - height / 2 - newBoundingBoxDimensions.height / 2, - CANVAS_GRID_SIZE_FINE - ), - }; + const newBoundingBoxCoordinates = { + x: roundToMultiple( + width / 2 - newBoundingBoxDimensions.width / 2, + CANVAS_GRID_SIZE_FINE + ), + y: roundToMultiple( + height / 2 - newBoundingBoxDimensions.height / 2, + CANVAS_GRID_SIZE_FINE + ), + }; - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions( - newBoundingBoxDimensions + if (state.boundingBoxScaleMethod === 'auto') { + const scaledDimensions = getScaledBoundingBoxDimensions( + newBoundingBoxDimensions, + optimalDimension + ); + state.scaledBoundingBoxDimensions = scaledDimensions; + } + + state.boundingBoxDimensions = newBoundingBoxDimensions; + state.boundingBoxCoordinates = newBoundingBoxCoordinates; + + state.pastLayerStates.push(cloneDeep(state.layerState)); + + state.layerState = { + ...cloneDeep(initialLayerState), + objects: [ + { + kind: 'image', + layer: 'base', + x: 0, + y: 0, + width, + height, + imageName: image_name, + }, + ], + }; + state.futureLayerStates = []; + state.batchIds = []; + + const newScale = calculateScale( + stageDimensions.width, + stageDimensions.height, + width, + height, + STAGE_PADDING_PERCENTAGE ); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - state.boundingBoxDimensions = newBoundingBoxDimensions; - state.boundingBoxCoordinates = newBoundingBoxCoordinates; - - state.pastLayerStates.push(cloneDeep(state.layerState)); - - state.layerState = { - ...cloneDeep(initialLayerState), - objects: [ - { - kind: 'image', - layer: 'base', - x: 0, - y: 0, - width: width, - height: height, - imageName: image.image_name, - }, - ], - }; - state.futureLayerStates = []; - state.batchIds = []; - - const newScale = calculateScale( - stageDimensions.width, - stageDimensions.height, - width, - height, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageDimensions.width, - stageDimensions.height, - 0, - 0, - width, - height, - newScale - ); - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - }, - setBoundingBoxDimensions: ( - state, - action: PayloadAction> - ) => { - const newDimensions = roundDimensionsToMultiple( - { - ...state.boundingBoxDimensions, - ...action.payload, + const newCoordinates = calculateCoordinates( + stageDimensions.width, + stageDimensions.height, + 0, + 0, + width, + height, + newScale + ); + state.stageScale = newScale; + state.stageCoordinates = newCoordinates; + }, + prepare: (payload: ImageDTO, optimalDimension: number) => ({ + payload, + meta: { + optimalDimension, }, - CANVAS_GRID_SIZE_FINE - ); - state.boundingBoxDimensions = newDimensions; - - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(newDimensions); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - }, - flipBoundingBoxAxes: (state) => { - const [currWidth, currHeight] = [ - state.boundingBoxDimensions.width, - state.boundingBoxDimensions.height, - ]; - const [currScaledWidth, currScaledHeight] = [ - state.scaledBoundingBoxDimensions.width, - state.scaledBoundingBoxDimensions.height, - ]; - state.boundingBoxDimensions = { - width: currHeight, - height: currWidth, - }; - state.scaledBoundingBoxDimensions = { - width: currScaledHeight, - height: currScaledWidth, - }; + }), }, setBoundingBoxCoordinates: (state, action: PayloadAction) => { state.boundingBoxCoordinates = floorCoordinates(action.payload); @@ -518,38 +520,6 @@ export const canvasSlice = createSlice({ state.stageScale = newScale; state.stageCoordinates = newCoordinates; - } else { - const newScale = calculateScale( - stageWidth, - stageHeight, - 512, - 512, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageWidth, - stageHeight, - 0, - 0, - 512, - 512, - newScale - ); - - const newBoundingBoxDimensions = { width: 512, height: 512 }; - - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - state.boundingBoxCoordinates = { x: 0, y: 0 }; - state.boundingBoxDimensions = newBoundingBoxDimensions; - - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions( - newBoundingBoxDimensions - ); - state.scaledBoundingBoxDimensions = scaledDimensions; - } } }, nextStagingAreaImage: (state) => { @@ -601,69 +571,29 @@ export const canvasSlice = createSlice({ state.shouldShowStagingImage = true; state.batchIds = []; }, - fitBoundingBoxToStage: (state) => { - const { - boundingBoxDimensions, - boundingBoxCoordinates, - stageDimensions, - stageScale, - } = state; - const scaledStageWidth = stageDimensions.width / stageScale; - const scaledStageHeight = stageDimensions.height / stageScale; + setBoundingBoxScaleMethod: { + reducer: ( + state, + action: PayloadActionWithOptimalDimension + ) => { + const boundingBoxScaleMethod = action.payload; + const { optimalDimension } = action.meta; + state.boundingBoxScaleMethod = boundingBoxScaleMethod; - if ( - boundingBoxCoordinates.x < 0 || - boundingBoxCoordinates.x + boundingBoxDimensions.width > - scaledStageWidth || - boundingBoxCoordinates.y < 0 || - boundingBoxCoordinates.y + boundingBoxDimensions.height > - scaledStageHeight - ) { - const newBoundingBoxDimensions = { - width: roundDownToMultiple( - clamp(scaledStageWidth, CANVAS_GRID_SIZE_FINE, 512), - CANVAS_GRID_SIZE_FINE - ), - height: roundDownToMultiple( - clamp(scaledStageHeight, CANVAS_GRID_SIZE_FINE, 512), - CANVAS_GRID_SIZE_FINE - ), - }; - - const newBoundingBoxCoordinates = { - x: roundToMultiple( - scaledStageWidth / 2 - newBoundingBoxDimensions.width / 2, - CANVAS_GRID_SIZE_FINE - ), - y: roundToMultiple( - scaledStageHeight / 2 - newBoundingBoxDimensions.height / 2, - CANVAS_GRID_SIZE_FINE - ), - }; - - state.boundingBoxDimensions = newBoundingBoxDimensions; - state.boundingBoxCoordinates = newBoundingBoxCoordinates; - - if (state.boundingBoxScaleMethod === 'auto') { + if (boundingBoxScaleMethod === 'auto') { const scaledDimensions = getScaledBoundingBoxDimensions( - newBoundingBoxDimensions + state.boundingBoxDimensions, + optimalDimension ); state.scaledBoundingBoxDimensions = scaledDimensions; } - } - }, - setBoundingBoxScaleMethod: ( - state, - action: PayloadAction - ) => { - state.boundingBoxScaleMethod = action.payload; - - if (action.payload === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions( - state.boundingBoxDimensions - ); - state.scaledBoundingBoxDimensions = scaledDimensions; - } + }, + prepare: (payload: BoundingBoxScaleMethod, optimalDimension: number) => ({ + payload, + meta: { + optimalDimension, + }, + }), }, setScaledBoundingBoxDimensions: ( state, @@ -671,6 +601,37 @@ export const canvasSlice = createSlice({ ) => { state.scaledBoundingBoxDimensions = action.payload; }, + setBoundingBoxDimensions: { + reducer: ( + state, + action: PayloadActionWithOptimalDimension> + ) => { + setBoundingBoxDimensionsReducer( + state, + action.payload, + action.meta.optimalDimension + ); + }, + prepare: (payload: Partial, optimalDimension: number) => ({ + payload, + meta: { + optimalDimension, + }, + }), + }, + scaledBoundingBoxDimensionsReset: { + reducer: (state, action: PayloadActionWithOptimalDimension) => { + const scaledDimensions = getScaledBoundingBoxDimensions( + state.boundingBoxDimensions, + action.meta.optimalDimension + ); + state.scaledBoundingBoxDimensions = scaledDimensions; + }, + prepare: (payload: void, optimalDimension: number) => ({ + payload: undefined, + meta: { optimalDimension }, + }), + }, setShouldShowStagingImage: (state, action: PayloadAction) => { state.shouldShowStagingImage = action.payload; }, @@ -714,6 +675,22 @@ export const canvasSlice = createSlice({ }, }, extraReducers: (builder) => { + builder.addCase(modelChanged, (state, action) => { + const optimalDimension = getOptimalDimension(action.payload); + const { width, height } = state.boundingBoxDimensions; + if (getIsSizeOptimal(width, height, optimalDimension)) { + return; + } + setBoundingBoxDimensionsReducer( + state, + { + width, + height, + }, + optimalDimension + ); + }); + builder.addCase(appSocketQueueItemStatusChanged, (state, action) => { const batch_status = action.payload.data.batch_status; if (!state.batchIds.includes(batch_status.batch_id)) { @@ -754,7 +731,6 @@ export const { commitColorPickerColor, commitStagingAreaImage, discardStagedImages, - fitBoundingBoxToStage, nextStagingAreaImage, prevStagingAreaImage, redo, @@ -764,7 +740,6 @@ export const { setBoundingBoxDimensions, setBoundingBoxPreviewFill, setBoundingBoxScaleMethod, - flipBoundingBoxAxes, setBrushColor, setBrushSize, setColorPickerColor, @@ -799,6 +774,7 @@ export const { canvasBatchIdAdded, canvasBatchIdsReset, aspectRatioChanged, + scaledBoundingBoxDimensionsReset, } = canvasSlice.actions; export default canvasSlice.reducer; diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts index c05fefc78d..4f8f217701 100644 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts +++ b/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts @@ -2,19 +2,22 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple'; import type { Dimensions } from 'features/canvas/store/canvasTypes'; import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; -const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => { +const getScaledBoundingBoxDimensions = ( + dimensions: Dimensions, + optimalDimension: number +) => { const { width, height } = dimensions; const scaledDimensions = { width, height }; - const targetArea = 512 * 512; + const targetArea = optimalDimension * optimalDimension; const aspectRatio = width / height; let currentArea = width * height; - let maxDimension = 448; + let maxDimension = optimalDimension - CANVAS_GRID_SIZE_FINE; while (currentArea < targetArea) { maxDimension += CANVAS_GRID_SIZE_FINE; if (width === height) { - scaledDimensions.width = 512; - scaledDimensions.height = 512; + scaledDimensions.width = optimalDimension; + scaledDimensions.height = optimalDimension; break; } else { if (aspectRatio > 1) { diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx index b381945321..80f0b6a59c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx @@ -17,6 +17,7 @@ import type { } from 'features/dnd/types'; import { heightChanged, + selectOptimalDimension, widthChanged, } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; @@ -37,8 +38,8 @@ type Props = { }; const selector = createMemoizedSelector( - stateSelector, - ({ controlAdapters, gallery, system }) => { + [stateSelector, activeTabNameSelector, selectOptimalDimension], + ({ controlAdapters, gallery, system }, activeTabName, optimalDimension) => { const { pendingControlImages } = controlAdapters; const { autoAddBoardId } = gallery; const { isConnected } = system; @@ -47,6 +48,8 @@ const selector = createMemoizedSelector( pendingControlImages, autoAddBoardId, isConnected, + activeTabName, + optimalDimension, }; } ); @@ -55,13 +58,15 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { const controlImageName = useControlAdapterControlImage(id); const processedControlImageName = useControlAdapterProcessedControlImage(id); const processorType = useControlAdapterProcessorType(id); - const dispatch = useAppDispatch(); const { t } = useTranslation(); - - const { pendingControlImages, autoAddBoardId, isConnected } = - useAppSelector(selector); - const activeTabName = useAppSelector(activeTabNameSelector); + const { + pendingControlImages, + autoAddBoardId, + isConnected, + activeTabName, + optimalDimension, + } = useAppSelector(selector); const [isMouseOverImage, setIsMouseOverImage] = useState(false); @@ -113,16 +118,19 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { if (activeTabName === 'unifiedCanvas') { dispatch( - setBoundingBoxDimensions({ - width: controlImage.width, - height: controlImage.height, - }) + setBoundingBoxDimensions( + { + width: controlImage.width, + height: controlImage.height, + }, + optimalDimension + ) ); } else { dispatch(widthChanged(controlImage.width)); dispatch(heightChanged(controlImage.height)); } - }, [controlImage, activeTabName, dispatch]); + }, [controlImage, activeTabName, dispatch, optimalDimension]); const handleMouseEnter = useCallback(() => { setIsMouseOverImage(true); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index aa80a206d6..dbcca55ff6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -2,7 +2,7 @@ import { Flex, Spinner } from '@chakra-ui/react'; import { useStore } from '@nanostores/react'; import { useAppToaster } from 'app/components/Toaster'; import { $customStarUI } from 'app/store/nanostores/customStarUI'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InvMenuItem } from 'common/components/InvMenu/InvMenuItem'; import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; @@ -17,6 +17,7 @@ import { } from 'features/gallery/store/actions'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; @@ -49,12 +50,10 @@ type SingleSelectionMenuItemsProps = { const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const { imageDTO } = props; - + const optimalDimension = useAppSelector(selectOptimalDimension); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const toaster = useAppToaster(); - const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const customStarUi = useStore($customStarUI); @@ -115,7 +114,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { flushSync(() => { dispatch(setActiveTab('unifiedCanvas')); }); - dispatch(setInitialCanvasImage(imageDTO)); + dispatch(setInitialCanvasImage(imageDTO, optimalDimension)); toaster({ title: t('toast.sentToUnifiedCanvas'), @@ -123,7 +122,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { duration: 2500, isClosable: true, }); - }, [dispatch, imageDTO, t, toaster]); + }, [dispatch, imageDTO, t, toaster, optimalDimension]); const handleUseAllParameters = useCallback(() => { recallAllParameters(metadata); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts index 5343aec49b..6ca263a903 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts @@ -1,6 +1,7 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import type { DenoiseLatentsInvocation, Edge, @@ -66,26 +67,20 @@ function copyConnectionsToDenoiseLatentsHrf(graph: NonNullableGraph): void { * Adjusts the width and height to maintain the aspect ratio and constrains them by the model's dimension limits, * rounding down to the nearest multiple of 8. * - * @param {string} baseModel The base model type, which determines the base dimension used in calculations. + * @param {number} optimalDimension The optimal dimension for the base model. * @param {number} width The current width to be adjusted for HRF. * @param {number} height The current height to be adjusted for HRF. * @return {{newWidth: number, newHeight: number}} The new width and height, adjusted and rounded as needed. */ function calculateHrfRes( - baseModel: string, + optimalDimension: number, width: number, height: number ): { newWidth: number; newHeight: number } { const aspect = width / height; - let dimension; - if (baseModel == 'sdxl') { - dimension = 1024; - } else { - dimension = 512; - } - const minDimension = Math.floor(dimension * 0.5); - const modelArea = dimension * dimension; // Assuming square images for model_area + const minDimension = Math.floor(optimalDimension * 0.5); + const modelArea = optimalDimension * optimalDimension; // Assuming square images for model_area let initWidth; let initHeight; @@ -126,11 +121,9 @@ export const addHrfToGraph = ( const isAutoVae = !vae; const width = state.generation.width; const height = state.generation.height; - const baseModel = state.generation.model - ? state.generation.model.base_model - : 'sd1'; + const optimalDimension = selectOptimalDimension(state); const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes( - baseModel, + optimalDimension, width, height ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx index 2c8cf1571c..a43750eacf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx @@ -1,5 +1,4 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { InvControl } from 'common/components/InvControl/InvControl'; import { InvSlider } from 'common/components/InvSlider/InvSlider'; @@ -9,21 +8,15 @@ import { CANVAS_GRID_SIZE_FINE, } from 'features/canvas/store/constants'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createMemoizedSelector( - [stateSelector, isStagingSelector], - ({ generation }, isStaging) => { - const { model } = generation; - const initial = ['sdxl', 'sdxl-refiner'].includes( - model?.base_model as string - ) - ? 1024 - : 512; + [selectOptimalDimension, isStagingSelector], + (optimalDimension, isStaging) => { return { - initial, - model, + initial: optimalDimension, isStaging, }; } diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx index b7540017e7..3e18dbbc4e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx @@ -1,5 +1,4 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { InvControl } from 'common/components/InvControl/InvControl'; import { InvSlider } from 'common/components/InvSlider/InvSlider'; @@ -9,21 +8,15 @@ import { CANVAS_GRID_SIZE_FINE, } from 'features/canvas/store/constants'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createMemoizedSelector( - [stateSelector, isStagingSelector], - ({ generation }, isStaging) => { - const { model } = generation; - const initial = ['sdxl', 'sdxl-refiner'].includes( - model?.base_model as string - ) - ? 1024 - : 512; + [selectOptimalDimension, isStagingSelector], + (optimalDimension, isStaging) => { return { - initial, - model, + initial: optimalDimension, isStaging, }; } diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx index 35642d8da9..021dcc50fc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx @@ -7,6 +7,7 @@ import type { } from 'common/components/InvSelect/types'; import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice'; import { isBoundingBoxScaleMethod } from 'features/canvas/store/canvasTypes'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,20 +19,20 @@ export const OPTIONS: InvSelectOption[] = [ const ParamScaleBeforeProcessing = () => { const dispatch = useAppDispatch(); + const { t } = useTranslation(); const boundingBoxScaleMethod = useAppSelector( (state) => state.canvas.boundingBoxScaleMethod ); - - const { t } = useTranslation(); + const optimalDimension = useAppSelector(selectOptimalDimension); const onChange = useCallback( (v) => { if (!isBoundingBoxScaleMethod(v?.value)) { return; } - dispatch(setBoundingBoxScaleMethod(v.value)); + dispatch(setBoundingBoxScaleMethod(v.value, optimalDimension)); }, - [dispatch] + [dispatch, optimalDimension] ); const value = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx index 007b0c209a..52bf6a590b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -5,18 +5,18 @@ import { InvControl } from 'common/components/InvControl/InvControl'; import { InvSlider } from 'common/components/InvSlider/InvSlider'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createMemoizedSelector( - [stateSelector], - ({ generation, canvas }) => { + [stateSelector, selectOptimalDimension], + ({ canvas }, optimalDimension) => { const { scaledBoundingBoxDimensions, boundingBoxScaleMethod, aspectRatio } = canvas; - const { model } = generation; return { - model, + optimalDimension, scaledBoundingBoxDimensions, isManual: boundingBoxScaleMethod === 'manual', aspectRatio, @@ -26,12 +26,12 @@ const selector = createMemoizedSelector( const ParamScaledHeight = () => { const dispatch = useAppDispatch(); - const { model, isManual, scaledBoundingBoxDimensions, aspectRatio } = - useAppSelector(selector); - - const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string) - ? 1024 - : 512; + const { + isManual, + scaledBoundingBoxDimensions, + aspectRatio, + optimalDimension, + } = useAppSelector(selector); const { t } = useTranslation(); @@ -56,7 +56,7 @@ const ParamScaledHeight = () => { const handleResetScaledHeight = useCallback(() => { let resetWidth = scaledBoundingBoxDimensions.width; - const resetHeight = Math.floor(initial); + const resetHeight = Math.floor(optimalDimension); if (aspectRatio) { resetWidth = roundToMultiple(resetHeight * aspectRatio.value, 64); @@ -68,7 +68,12 @@ const ParamScaledHeight = () => { height: resetHeight, }) ); - }, [aspectRatio, dispatch, initial, scaledBoundingBoxDimensions.width]); + }, [ + aspectRatio, + dispatch, + optimalDimension, + scaledBoundingBoxDimensions.width, + ]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx index d8ecfa6f17..28ff0d0f8b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -5,18 +5,18 @@ import { InvControl } from 'common/components/InvControl/InvControl'; import { InvSlider } from 'common/components/InvSlider/InvSlider'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createMemoizedSelector( - [stateSelector], - ({ canvas, generation }) => { + [stateSelector, selectOptimalDimension], + ({ canvas }, optimalDimension) => { const { boundingBoxScaleMethod, scaledBoundingBoxDimensions, aspectRatio } = canvas; - const { model } = generation; return { - model, + initial: optimalDimension, scaledBoundingBoxDimensions, aspectRatio, isManual: boundingBoxScaleMethod === 'manual', @@ -26,13 +26,9 @@ const selector = createMemoizedSelector( const ParamScaledWidth = () => { const dispatch = useAppDispatch(); - const { model, isManual, scaledBoundingBoxDimensions, aspectRatio } = + const { initial, isManual, scaledBoundingBoxDimensions, aspectRatio } = useAppSelector(selector); - const initial = ['sdxl', 'sdxl-refiner'].includes(model?.base_model as string) - ? 1024 - : 512; - const { t } = useTranslation(); const handleChangeScaledWidth = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx index a743e172ee..f9647d5b8b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamHeight.tsx @@ -5,23 +5,17 @@ import { InvControl } from 'common/components/InvControl/InvControl'; import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput'; import { InvSlider } from 'common/components/InvSlider/InvSlider'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createMemoizedSelector( - [stateSelector], - ({ generation, config }) => { + [stateSelector, selectOptimalDimension], + ({ config }, optimalDimension) => { const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height; - const { model } = generation; - - const initial = ['sdxl', 'sdxl-refiner'].includes( - model?.base_model as string - ) - ? 1024 - : 512; return { - initial, + initial: optimalDimension, min, max: sliderMax, inputMax, diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx index ab03862209..7926c603d5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWidth.tsx @@ -5,23 +5,17 @@ import { InvControl } from 'common/components/InvControl/InvControl'; import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput'; import { InvSlider } from 'common/components/InvSlider/InvSlider'; import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const selector = createMemoizedSelector( - [stateSelector], - ({ generation, config }) => { + [stateSelector, selectOptimalDimension], + ({ config }, optimalDimension) => { const { min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width; - const { model } = generation; - - const initial = ['sdxl', 'sdxl-refiner'].includes( - model?.base_model as string - ) - ? 1024 - : 512; return { - initial, + initial: optimalDimension, min, max: sliderMax, step: coarseStep, diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts index e9d573387d..68bc593a35 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/ImageSizeContext.ts @@ -1,3 +1,4 @@ +import { useAppSelector } from 'app/store/storeHooks'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { @@ -8,6 +9,7 @@ import type { AspectRatioID, AspectRatioState, } from 'features/parameters/components/ImageSize/types'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { createContext, useCallback, useContext, useMemo } from 'react'; export type ImageSizeContextInnerValue = { @@ -28,7 +30,7 @@ export type ImageSizeContext = { widthChanged: (width: number) => void; heightChanged: (height: number) => void; isLockedToggled: () => void; - sizeReset: (width: number, height: number) => void; + setOptimalSize: () => void; }; export const ImageSizeContext = @@ -36,6 +38,7 @@ export const ImageSizeContext = export const useImageSizeContext = (): ImageSizeContext => { const _ctx = useContext(ImageSizeContext); + const optimalDimension = useAppSelector(selectOptimalDimension); if (!_ctx) { throw new Error( @@ -58,8 +61,7 @@ export const useImageSizeContext = (): ImageSizeContext => { state.value = ASPECT_RATIO_MAP[state.id].ratio; const { width, height } = calculateNewSize( state.value, - _ctx.width, - _ctx.height + _ctx.width * _ctx.height ); _ctx.onChangeWidth(width); _ctx.onChangeHeight(height); @@ -84,8 +86,7 @@ export const useImageSizeContext = (): ImageSizeContext => { // Else we need to calculate the new size const { width, height } = calculateNewSize( state.value, - _ctx.width, - _ctx.height + _ctx.width * _ctx.height ); _ctx.onChangeWidth(width); _ctx.onChangeHeight(height); @@ -141,15 +142,20 @@ export const useImageSizeContext = (): ImageSizeContext => { _ctx.onChangeAspectRatioState(state); }, [_ctx]); - const sizeReset = useCallback( - (width: number, height: number) => { - const state = { ...initialAspectRatioState }; - _ctx.onChangeAspectRatioState(state); + const setOptimalSize = useCallback(() => { + if (_ctx.aspectRatioState.isLocked) { + const { width, height } = calculateNewSize( + _ctx.aspectRatioState.value, + optimalDimension * optimalDimension + ); _ctx.onChangeWidth(width); _ctx.onChangeHeight(height); - }, - [_ctx] - ); + } else { + _ctx.onChangeAspectRatioState({ ...initialAspectRatioState }); + _ctx.onChangeWidth(optimalDimension); + _ctx.onChangeHeight(optimalDimension); + } + }, [_ctx, optimalDimension]); const ctx = useMemo( () => ({ @@ -159,7 +165,7 @@ export const useImageSizeContext = (): ImageSizeContext => { widthChanged, heightChanged, isLockedToggled, - sizeReset, + setOptimalSize, }), [ _ctx, @@ -167,7 +173,7 @@ export const useImageSizeContext = (): ImageSizeContext => { dimensionsSwapped, heightChanged, isLockedToggled, - sizeReset, + setOptimalSize, widthChanged, ] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx index d5ab2ac2a0..869cf73d02 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/LockAspectRatioButton.tsx @@ -13,6 +13,7 @@ export const LockAspectRatioButton = memo(() => { return ( { const { t } = useTranslation(); const ctx = useImageSizeContext(); - const optimalDimension = useAppSelector((state) => - state.generation.model?.base_model === 'sdxl' ? 1024 : 512 + const optimalDimension = useAppSelector(selectOptimalDimension); + const isSizeTooSmall = useMemo( + () => getIsSizeTooSmall(ctx.width, ctx.height, optimalDimension), + [ctx.height, ctx.width, optimalDimension] + ); + const isSizeTooLarge = useMemo( + () => getIsSizeTooLarge(ctx.width, ctx.height, optimalDimension), + [ctx.height, ctx.width, optimalDimension] ); const onClick = useCallback(() => { - ctx.sizeReset(optimalDimension, optimalDimension); - }, [ctx, optimalDimension]); + ctx.setOptimalSize(); + }, [ctx]); + const tooltip = useMemo(() => { + if (isSizeTooSmall) { + return t('parameters.setToOptimalSizeTooSmall'); + } + if (isSizeTooLarge) { + return t('parameters.setToOptimalSizeTooLarge'); + } + return t('parameters.setToOptimalSize'); + }, [isSizeTooLarge, isSizeTooSmall, t]); return ( } + colorScheme={isSizeTooSmall || isSizeTooLarge ? 'warning' : 'base'} /> ); }); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx index 136aba03a6..d5d0921bea 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/SwapDimensionsButton.tsx @@ -12,6 +12,7 @@ export const SwapDimensionsButton = memo(() => { }, [ctx]); return ( { - const area = width * height; - const newWidth = roundToMultiple(Math.sqrt(area * ratio), 8); - const newHeight = roundToMultiple(area / newWidth, 8); - return { width: newWidth, height: newHeight }; + const exactWidth = Math.sqrt(area * ratio); + const exactHeight = exactWidth / ratio; + + return { + width: roundToMultiple(exactWidth, 8), + height: roundToMultiple(exactHeight, 8), + }; }; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index 507503af33..138cecd094 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts @@ -1,8 +1,9 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppToaster } from 'app/components/Toaster'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { initialImageSelected } from 'features/parameters/store/actions'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { t } from 'i18next'; import { useCallback, useEffect } from 'react'; @@ -18,8 +19,8 @@ export const usePreselectedImage = (selectedImage?: { action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; }) => { const dispatch = useAppDispatch(); - const { recallAllParameters } = useRecallParameters(); + const optimalDimension = useAppSelector(selectOptimalDimension); const toaster = useAppToaster(); const { currentData: selectedImageDto } = useGetImageDTOQuery( @@ -32,7 +33,7 @@ export const usePreselectedImage = (selectedImage?: { const handleSendToCanvas = useCallback(() => { if (selectedImageDto) { - dispatch(setInitialCanvasImage(selectedImageDto)); + dispatch(setInitialCanvasImage(selectedImageDto, optimalDimension)); dispatch(setActiveTab('unifiedCanvas')); toaster({ title: t('toast.sentToUnifiedCanvas'), @@ -41,7 +42,7 @@ export const usePreselectedImage = (selectedImage?: { isClosable: true, }); } - }, [dispatch, toaster, selectedImageDto]); + }, [selectedImageDto, dispatch, optimalDimension, toaster]); const handleSendToImg2Img = useCallback(() => { if (selectedImageDto) { diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 8a5526b3bd..142cf5413a 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -2,6 +2,7 @@ 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 { 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'; @@ -16,6 +17,10 @@ import type { ParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; +import { + getIsSizeOptimal, + getOptimalDimension, +} from 'features/parameters/util/optimalDimension'; import { configChanged } from 'features/system/store/configSlice'; import { clamp } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; @@ -198,15 +203,26 @@ export const generationSlice = createSlice({ state.initialImage = { imageName: image_name, width, height }; }, modelChanged: (state, action: PayloadAction) => { - state.model = action.payload; + const newModel = action.payload; + state.model = newModel; - if (state.model === null) { + if (newModel === null) { return; } // Clamp ClipSkip Based On Selected Model - const { maxClip } = CLIP_SKIP_MAP[state.model.base_model]; + const { maxClip } = CLIP_SKIP_MAP[newModel.base_model]; state.clipSkip = clamp(state.clipSkip, 0, maxClip); + 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; }, vaeSelected: (state, action: PayloadAction) => { // null is a valid VAE! @@ -259,6 +275,9 @@ export const generationSlice = createSlice({ } }); }, + selectors: { + selectOptimalDimension: (slice) => getOptimalDimension(slice.model), + }, }); export const { @@ -306,4 +325,6 @@ export const { heightChanged, } = generationSlice.actions; +export const { selectOptimalDimension } = generationSlice.selectors; + export default generationSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/store/types.ts b/invokeai/frontend/web/src/features/parameters/store/types.ts index e78acfbfd3..cd42fb1340 100644 --- a/invokeai/frontend/web/src/features/parameters/store/types.ts +++ b/invokeai/frontend/web/src/features/parameters/store/types.ts @@ -1,3 +1,4 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterCanvasCoherenceMode, @@ -58,3 +59,9 @@ export interface GenerationState { shouldShowAdvancedOptions: boolean; aspectRatio: AspectRatioState; } + +export type PayloadActionWithOptimalDimension = PayloadAction< + T, + string, + { optimalDimension: number } +>; diff --git a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts new file mode 100644 index 0000000000..28bbd1e62f --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts @@ -0,0 +1,57 @@ +import type { ModelIdentifier } from 'features/nodes/types/common'; + +/** + * 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 + */ +export const getOptimalDimension = (model?: ModelIdentifier | null): number => + model?.base_model === 'sdxl' ? 1024 : 512; + +const MIN_AREA_FACTOR = 0.8; +const MAX_AREA_FACTOR = 1.2; + +export const getIsSizeTooSmall = ( + width: number, + height: number, + optimalDimension: number +): boolean => { + const currentArea = width * height; + const optimalArea = optimalDimension * optimalDimension; + if (currentArea < optimalArea * MIN_AREA_FACTOR) { + return true; + } + return false; +}; + +export const getIsSizeTooLarge = ( + width: number, + height: number, + optimalDimension: number +): boolean => { + const currentArea = width * height; + const optimalArea = optimalDimension * optimalDimension; + if (currentArea > optimalArea * MAX_AREA_FACTOR) { + return true; + } + return false; +}; + +/** + * Gets whether the current width and height needs to be resized to the optimal dimension. + * The current width and height needs to be resized if the current area is not within 20% of the optimal area. + * @param width The width to compare with the optimal dimension + * @param height The height to compare with the optimal dimension + * @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 => { + return ( + !getIsSizeTooSmall(width, height, optimalDimension) && + !getIsSizeTooLarge(width, height, optimalDimension) + ); +}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeCanvas.tsx b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeCanvas.tsx index 648a14d0f0..3317b7224e 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeCanvas.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/ImageSettingsAccordion/ImageSizeCanvas.tsx @@ -7,6 +7,7 @@ import ParamBoundingBoxHeight from 'features/parameters/components/Canvas/Boundi import ParamBoundingBoxWidth from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth'; import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { memo, useCallback } from 'react'; export const ImageSizeCanvas = memo(() => { @@ -15,19 +16,20 @@ export const ImageSizeCanvas = memo(() => { (state) => state.canvas.boundingBoxDimensions ); const aspectRatioState = useAppSelector((state) => state.canvas.aspectRatio); + const optimalDimension = useAppSelector(selectOptimalDimension); const onChangeWidth = useCallback( (width: number) => { - dispatch(setBoundingBoxDimensions({ width })); + dispatch(setBoundingBoxDimensions({ width }, optimalDimension)); }, - [dispatch] + [dispatch, optimalDimension] ); const onChangeHeight = useCallback( (height: number) => { - dispatch(setBoundingBoxDimensions({ height })); + dispatch(setBoundingBoxDimensions({ height }, optimalDimension)); }, - [dispatch] + [dispatch, optimalDimension] ); const onChangeAspectRatioState = useCallback( diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 97cba57fcf..4e9a0f2b61 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -27,10 +27,7 @@ import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged, } from 'features/system/store/systemSlice'; -import { - setShouldAutoChangeDimensions, - setShouldShowProgressInViewer, -} from 'features/ui/store/uiSlice'; +import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice'; import type { ChangeEvent, ReactElement } from 'react'; import { cloneElement, memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -54,7 +51,7 @@ const selector = createMemoizedSelector( shouldEnableInformationalPopovers, } = system; const { shouldUseCpuNoise } = generation; - const { shouldShowProgressInViewer, shouldAutoChangeDimensions } = ui; + const { shouldShowProgressInViewer } = ui; return { shouldUseCpuNoise, @@ -65,7 +62,6 @@ const selector = createMemoizedSelector( shouldAntialiasProgressImage, shouldUseNSFWChecker, shouldUseWatermarker, - shouldAutoChangeDimensions, shouldEnableInformationalPopovers, }; } @@ -135,7 +131,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { shouldAntialiasProgressImage, shouldUseNSFWChecker, shouldUseWatermarker, - shouldAutoChangeDimensions, shouldEnableInformationalPopovers, } = useAppSelector(selector); @@ -191,12 +186,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { }, [dispatch] ); - const handleChangeShouldAutoChangeDimensions = useCallback( - (e: ChangeEvent) => { - dispatch(setShouldAutoChangeDimensions(e.target.checked)); - }, - [dispatch] - ); const handleChangeShouldEnableInformationalPopovers = useCallback( (e: ChangeEvent) => { dispatch(setShouldEnableInformationalPopovers(e.target.checked)); @@ -280,12 +269,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { onChange={handleChangeShouldAntialiasProgressImage} /> - - - ) => { state.shouldShowProgressInViewer = action.payload; }, - setShouldAutoChangeDimensions: (state, action: PayloadAction) => { - state.shouldAutoChangeDimensions = action.payload; - }, panelsChanged: ( state, action: PayloadAction<{ name: string; value: string }> @@ -60,7 +56,6 @@ export const { setShouldShowExistingModelsInSearch, setShouldHidePreview, setShouldShowProgressInViewer, - setShouldAutoChangeDimensions, panelsChanged, } = uiSlice.actions; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 7dbd77b995..e87026d6b0 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,23 +1,10 @@ import type { InvokeTabName } from './tabMap'; -export type Coordinates = { - x: number; - y: number; -}; - -export type Dimensions = { - width: number | string; - height: number | string; -}; - -export type Rect = Coordinates & Dimensions; - export interface UIState { activeTab: InvokeTabName; shouldShowImageDetails: boolean; shouldShowExistingModelsInSearch: boolean; shouldHidePreview: boolean; shouldShowProgressInViewer: boolean; - shouldAutoChangeDimensions: boolean; panels: Record; }