diff --git a/frontend/src/app/features.ts b/frontend/src/app/features.ts index f09effea09..0480e443fe 100644 --- a/frontend/src/app/features.ts +++ b/frontend/src/app/features.ts @@ -14,7 +14,8 @@ export enum Feature { FACE_CORRECTION, IMAGE_TO_IMAGE, BOUNDING_BOX, - CANVAS_COMPOSITION, + SEAM_CORRECTION, + INFILL_AND_SCALING, } /** For each tooltip in the UI, the below feature definitions & props will pull relevant information into the tooltip. * @@ -57,17 +58,22 @@ export const FEATURES: Record = { guideImage: 'asset/path.gif', }, [Feature.IMAGE_TO_IMAGE]: { - text: 'ImageToImage allows the upload of an initial image, which InvokeAI will use to guide the generation process, along with a prompt. A lower value for this setting will more closely resemble the original image. Values between 0-1 are accepted, and a range of .25-.75 is recommended ', + text: 'Image to Image allows the upload of an initial image, which InvokeAI will use to guide the generation process, along with a prompt. A lower value for this setting will more closely resemble the original image. Values between 0-1 are accepted, and a range of .25-.75 is recommended ', href: 'link/to/docs/feature3.html', guideImage: 'asset/path.gif', }, [Feature.BOUNDING_BOX]: { - text: 'The Bounding Box is analogous to the Width and Height settings for Text to Image or Image to Image. Only the area in the box will be processed.', + text: 'The bounding box is analogous to the Width and Height settings for Text to Image or Image to Image. Only the area in the box will be processed.', href: 'link/to/docs/feature3.html', guideImage: 'asset/path.gif', }, - [Feature.CANVAS_COMPOSITION]: { - text: 'Control the process used to cleanly manage seams between existing compositions and new invocations, using larger areas of the image for seam guidance, or applying various configurations on the generation process.', + [Feature.SEAM_CORRECTION]: { + text: 'Control the handling of visible seams which may occur when a generated image is pasted back onto the canvas.', + href: 'link/to/docs/feature3.html', + guideImage: 'asset/path.gif', + }, + [Feature.INFILL_AND_SCALING]: { + text: 'Manage infill methods (used on masked or erased areas of the canvas) and scaling (useful for small bounding box sizes).', href: 'link/to/docs/feature3.html', guideImage: 'asset/path.gif', }, diff --git a/frontend/src/common/components/IAISelect.scss b/frontend/src/common/components/IAISelect.scss index d4db363062..51baa3bbaa 100644 --- a/frontend/src/common/components/IAISelect.scss +++ b/frontend/src/common/components/IAISelect.scss @@ -14,6 +14,7 @@ border: 2px solid var(--border-color); background-color: var(--background-color-secondary); font-weight: bold; + font-size: 0.9rem; height: 2rem; border-radius: 0.2rem; diff --git a/frontend/src/common/util/parameterTranslation.ts b/frontend/src/common/util/parameterTranslation.ts index db6ea02b4b..ec4b2f9ef8 100644 --- a/frontend/src/common/util/parameterTranslation.ts +++ b/frontend/src/common/util/parameterTranslation.ts @@ -127,7 +127,7 @@ export const frontendToBackendParameters = ( stageScale, isMaskEnabled, shouldPreserveMaskedArea, - shouldScaleBoundingBox, + boundingBoxScaleMethod: boundingBoxScale, scaledBoundingBoxDimensions, } = canvasState; @@ -185,7 +185,7 @@ export const frontendToBackendParameters = ( generationParameters.progress_images = false; - if (shouldScaleBoundingBox) { + if (boundingBoxScale !== 'none') { generationParameters.inpaint_width = scaledBoundingBoxDimensions.width; generationParameters.inpaint_height = scaledBoundingBoxDimensions.height; } diff --git a/frontend/src/features/canvas/components/IAICanvasStatusText.tsx b/frontend/src/features/canvas/components/IAICanvasStatusText.tsx index 3128a1280a..099c322313 100644 --- a/frontend/src/features/canvas/components/IAICanvasStatusText.tsx +++ b/frontend/src/features/canvas/components/IAICanvasStatusText.tsx @@ -12,10 +12,15 @@ const selector = createSelector( stageDimensions: { width: stageWidth, height: stageHeight }, stageCoordinates: { x: stageX, y: stageY }, boundingBoxDimensions: { width: boxWidth, height: boxHeight }, + scaledBoundingBoxDimensions: { + width: scaledBoxWidth, + height: scaledBoxHeight, + }, boundingBoxCoordinates: { x: boxX, y: boxY }, stageScale, shouldShowCanvasDebugInfo, layer, + boundingBoxScaleMethod, } = canvas; return { @@ -30,12 +35,14 @@ const selector = createSelector( boxX )}, ${roundToHundreth(boxY)})`, boundingBoxDimensionsString: `${boxWidth}×${boxHeight}`, + scaledBoundingBoxDimensionsString: `${scaledBoxWidth}×${scaledBoxHeight}`, canvasCoordinatesString: `${roundToHundreth(stageX)}×${roundToHundreth( stageY )}`, canvasDimensionsString: `${stageWidth}×${stageHeight}`, canvasScaleString: Math.round(stageScale * 100), shouldShowCanvasDebugInfo, + shouldShowScaledBoundingBox: boundingBoxScaleMethod !== 'none', }; }, { @@ -52,6 +59,8 @@ const IAICanvasStatusText = () => { boundingBoxColor, boundingBoxCoordinatesString, boundingBoxDimensionsString, + scaledBoundingBoxDimensionsString, + shouldShowScaledBoundingBox, canvasCoordinatesString, canvasDimensionsString, canvasScaleString, @@ -71,6 +80,13 @@ const IAICanvasStatusText = () => { color: boundingBoxColor, }} >{`Bounding Box: ${boundingBoxDimensionsString}`} + {shouldShowScaledBoundingBox && ( +
{`Scaled Bounding Box: ${scaledBoundingBoxDimensionsString}`}
+ )} {shouldShowCanvasDebugInfo && ( <>
{`Bounding Box Position: ${boundingBoxCoordinatesString}`}
diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx index d88071ebfc..65672ee693 100644 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx @@ -18,6 +18,8 @@ import IAICheckbox from 'common/components/IAICheckbox'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import EmptyTempFolderButtonModal from 'features/system/components/ClearTempFolderButtonModal'; import ClearCanvasHistoryButtonModal from '../ClearCanvasHistoryButtonModal'; +import { ChangeEvent } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; export const canvasControlsSelector = createSelector( [canvasSelector], @@ -61,6 +63,21 @@ const IAICanvasSettingsButtonPopover = () => { shouldSnapToGrid, } = useAppSelector(canvasControlsSelector); + useHotkeys( + ['n'], + () => { + dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); + }, + { + enabled: true, + preventDefault: true, + }, + [shouldSnapToGrid] + ); + + const handleChangeShouldSnapToGrid = (e: ChangeEvent) => + dispatch(setShouldSnapToGrid(e.target.checked)); + return ( { dispatch(setShouldSnapToGrid(e.target.checked))} + onChange={handleChangeShouldSnapToGrid} /> ) => { - state.boundingBoxDimensions = roundDimensionsTo64(action.payload); + const newDimensions = roundDimensionsTo64(action.payload); + state.boundingBoxDimensions = newDimensions; + + if (state.boundingBoxScaleMethod === 'auto') { + const { width, height } = newDimensions; + const newScaledDimensions = { width, height }; + const targetArea = 512 * 512; + const aspectRatio = width / height; + let currentArea = width * height; + let maxDimension = 448; + while (currentArea < targetArea) { + maxDimension += 64; + if (width === height) { + newScaledDimensions.width = 512; + newScaledDimensions.height = 512; + break; + } else { + if (aspectRatio > 1) { + newScaledDimensions.width = maxDimension; + newScaledDimensions.height = roundToMultiple( + maxDimension / aspectRatio, + 64 + ); + } else if (aspectRatio < 1) { + newScaledDimensions.height = maxDimension; + newScaledDimensions.width = roundToMultiple( + maxDimension * aspectRatio, + 64 + ); + } + currentArea = + newScaledDimensions.width * newScaledDimensions.height; + } + } + + state.scaledBoundingBoxDimensions = newScaledDimensions; + } }, setBoundingBoxCoordinates: (state, action: PayloadAction) => { state.boundingBoxCoordinates = floorCoordinates(action.payload); @@ -671,8 +708,11 @@ export const canvasSlice = createSlice({ state.boundingBoxCoordinates = newBoundingBoxCoordinates; } }, - setShouldScaleBoundingBox: (state, action: PayloadAction) => { - state.shouldScaleBoundingBox = action.payload; + setBoundingBoxScaleMethod: ( + state, + action: PayloadAction + ) => { + state.boundingBoxScaleMethod = action.payload; }, setScaledBoundingBoxDimensions: ( state, @@ -748,6 +788,7 @@ export const { setBoundingBoxCoordinates, setBoundingBoxDimensions, setBoundingBoxPreviewFill, + setBoundingBoxScaleMethod, setBrushColor, setBrushSize, setCanvasContainerDimensions, @@ -772,7 +813,6 @@ export const { setShouldDarkenOutsideBoundingBox, setShouldLockBoundingBox, setShouldPreserveMaskedArea, - setShouldScaleBoundingBox, setShouldShowBoundingBox, setShouldShowBrush, setShouldShowBrushPreview, diff --git a/frontend/src/features/canvas/store/canvasTypes.ts b/frontend/src/features/canvas/store/canvasTypes.ts index 2817faf0c8..8686e50ff3 100644 --- a/frontend/src/features/canvas/store/canvasTypes.ts +++ b/frontend/src/features/canvas/store/canvasTypes.ts @@ -11,6 +11,16 @@ export const LAYER_NAMES = ['base', 'mask'] as const; export type CanvasLayer = typeof LAYER_NAMES[number]; +export const BOUNDING_BOX_SCALES_DICT = [ + { key: 'Auto', value: 'auto' }, + { key: 'Manual', value: 'manual' }, + { key: 'None', value: 'none' }, +]; + +export const BOUNDING_BOX_SCALES = ['none', 'auto', 'manual'] as const; + +export type BoundingBoxScale = typeof BOUNDING_BOX_SCALES[number]; + export type CanvasDrawingTool = 'brush' | 'eraser'; export type CanvasTool = CanvasDrawingTool | 'move' | 'colorPicker'; @@ -78,6 +88,7 @@ export interface CanvasState { boundingBoxCoordinates: Vector2d; boundingBoxDimensions: Dimensions; boundingBoxPreviewFill: RgbaColor; + boundingBoxScaleMethod: BoundingBoxScale; brushColor: RgbaColor; brushSize: number; canvasContainerDimensions: Dimensions; @@ -108,7 +119,6 @@ export interface CanvasState { shouldDarkenOutsideBoundingBox: boolean; shouldLockBoundingBox: boolean; shouldPreserveMaskedArea: boolean; - shouldScaleBoundingBox: boolean; shouldShowBoundingBox: boolean; shouldShowBrush: boolean; shouldShowBrushPreview: boolean; diff --git a/frontend/src/features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings.tsx b/frontend/src/features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings.tsx index 797c3c76c7..6d35a496f8 100644 --- a/frontend/src/features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings.tsx +++ b/frontend/src/features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings.tsx @@ -2,27 +2,17 @@ import { Box, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store'; import IAISlider from 'common/components/IAISlider'; -import IAISwitch from 'common/components/IAISwitch'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; -import { - setBoundingBoxDimensions, - setScaledBoundingBoxDimensions, - setShouldScaleBoundingBox, -} from 'features/canvas/store/canvasSlice'; +import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import _ from 'lodash'; const selector = createSelector( canvasSelector, (canvas) => { - const { - boundingBoxDimensions, - shouldScaleBoundingBox, - scaledBoundingBoxDimensions, - } = canvas; + const { boundingBoxDimensions, boundingBoxScaleMethod: boundingBoxScale } = canvas; return { boundingBoxDimensions, - shouldScaleBoundingBox, - scaledBoundingBoxDimensions, + boundingBoxScale, }; }, { @@ -34,11 +24,7 @@ const selector = createSelector( const BoundingBoxSettings = () => { const dispatch = useAppDispatch(); - const { - boundingBoxDimensions, - shouldScaleBoundingBox, - scaledBoundingBoxDimensions, - } = useAppSelector(selector); + const { boundingBoxDimensions } = useAppSelector(selector); const handleChangeWidth = (v: number) => { dispatch( @@ -76,42 +62,6 @@ const BoundingBoxSettings = () => { ); }; - const handleChangeScaledWidth = (v: number) => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - width: Math.floor(v), - }) - ); - }; - - const handleChangeScaledHeight = (v: number) => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - height: Math.floor(v), - }) - ); - }; - - const handleResetScaledWidth = () => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - width: Math.floor(512), - }) - ); - }; - - const handleResetScaledHeight = () => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - height: Math.floor(512), - }) - ); - }; - return ( { withInput withReset /> - dispatch(setShouldScaleBoundingBox(e.target.checked))} - /> - - ); }; diff --git a/frontend/src/features/options/components/AdvancedOptions/Canvas/InfillAndScalingOptions.tsx b/frontend/src/features/options/components/AdvancedOptions/Canvas/InfillAndScalingOptions.tsx new file mode 100644 index 0000000000..705eb32e9f --- /dev/null +++ b/frontend/src/features/options/components/AdvancedOptions/Canvas/InfillAndScalingOptions.tsx @@ -0,0 +1,182 @@ +import { Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store'; +import IAISelect from 'common/components/IAISelect'; +import IAISlider from 'common/components/IAISlider'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { + setBoundingBoxDimensions, + setBoundingBoxScaleMethod, + setScaledBoundingBoxDimensions, +} from 'features/canvas/store/canvasSlice'; +import { + BoundingBoxScale, + BOUNDING_BOX_SCALES_DICT, +} from 'features/canvas/store/canvasTypes'; +import { optionsSelector } from 'features/options/store/optionsSelectors'; +import { + setInfillMethod, + setTileSize, +} from 'features/options/store/optionsSlice'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import _ from 'lodash'; +import { ChangeEvent } from 'react'; +import InpaintReplace from './InpaintReplace'; + +const selector = createSelector( + [optionsSelector, systemSelector, canvasSelector], + (options, system, canvas) => { + const { tileSize, infillMethod } = options; + + const { infill_methods: availableInfillMethods } = system; + + const { + boundingBoxDimensions, + boundingBoxScaleMethod: boundingBoxScale, + scaledBoundingBoxDimensions, + } = canvas; + + return { + boundingBoxDimensions, + boundingBoxScale, + scaledBoundingBoxDimensions, + tileSize, + infillMethod, + availableInfillMethods, + isManual: boundingBoxScale === 'manual', + }; + }, + { + memoizeOptions: { + resultEqualityCheck: _.isEqual, + }, + } +); + +const InfillAndScalingOptions = () => { + const dispatch = useAppDispatch(); + const { + tileSize, + infillMethod, + boundingBoxDimensions, + availableInfillMethods, + boundingBoxScale, + isManual, + scaledBoundingBoxDimensions, + } = useAppSelector(selector); + + const handleChangeScaledWidth = (v: number) => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + width: Math.floor(v), + }) + ); + }; + + const handleChangeScaledHeight = (v: number) => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + height: Math.floor(v), + }) + ); + }; + + const handleResetScaledWidth = () => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + width: Math.floor(512), + }) + ); + }; + + const handleResetScaledHeight = () => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + height: Math.floor(512), + }) + ); + }; + + const handleChangeBoundingBoxScaleMethod = ( + e: ChangeEvent + ) => { + dispatch(setBoundingBoxScaleMethod(e.target.value as BoundingBoxScale)); + dispatch(setBoundingBoxDimensions(boundingBoxDimensions)); + }; + + return ( + + + + + + dispatch(setInfillMethod(e.target.value))} + /> + { + dispatch(setTileSize(v)); + }} + handleReset={() => { + dispatch(setTileSize(32)); + }} + withInput + withSliderMarks + withReset + /> + + ); +}; + +export default InfillAndScalingOptions; diff --git a/frontend/src/features/options/components/AdvancedOptions/Canvas/CompositionOptions.tsx b/frontend/src/features/options/components/AdvancedOptions/Canvas/SeamCorrectionOptions.tsx similarity index 61% rename from frontend/src/features/options/components/AdvancedOptions/Canvas/CompositionOptions.tsx rename to frontend/src/features/options/components/AdvancedOptions/Canvas/SeamCorrectionOptions.tsx index 938c93c7a1..561b9c78ea 100644 --- a/frontend/src/features/options/components/AdvancedOptions/Canvas/CompositionOptions.tsx +++ b/frontend/src/features/options/components/AdvancedOptions/Canvas/SeamCorrectionOptions.tsx @@ -1,43 +1,26 @@ import { Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store'; -import IAISelect from 'common/components/IAISelect'; import IAISlider from 'common/components/IAISlider'; import { optionsSelector } from 'features/options/store/optionsSelectors'; import { - setInfillMethod, setSeamBlur, setSeamSize, setSeamSteps, setSeamStrength, - setTileSize, } from 'features/options/store/optionsSlice'; -import { systemSelector } from 'features/system/store/systemSelectors'; import _ from 'lodash'; -import InpaintReplace from './InpaintReplace'; const selector = createSelector( - [optionsSelector, systemSelector], - (options, system) => { - const { - seamSize, - seamBlur, - seamStrength, - seamSteps, - tileSize, - infillMethod, - } = options; - - const { infill_methods: availableInfillMethods } = system; + [optionsSelector], + (options) => { + const { seamSize, seamBlur, seamStrength, seamSteps } = options; return { seamSize, seamBlur, seamStrength, seamSteps, - tileSize, - infillMethod, - availableInfillMethods, }; }, { @@ -47,22 +30,13 @@ const selector = createSelector( } ); -const CompositionOptions = () => { +const SeamCorrectionOptions = () => { const dispatch = useAppDispatch(); - const { - seamSize, - seamBlur, - seamStrength, - seamSteps, - tileSize, - infillMethod, - availableInfillMethods, - } = useAppSelector(selector); + const { seamSize, seamBlur, seamStrength, seamSteps } = + useAppSelector(selector); return ( - - { withSliderMarks withReset /> - dispatch(setInfillMethod(e.target.value))} - /> - {infillMethod === 'tile' && ( - { - dispatch(setTileSize(v)); - }} - handleReset={() => { - dispatch(setTileSize(32)); - }} - withInput - withSliderMarks - withReset - /> - )} ); }; -export default CompositionOptions; +export default SeamCorrectionOptions; diff --git a/frontend/src/features/system/components/HotkeysModal/HotkeysModal.tsx b/frontend/src/features/system/components/HotkeysModal/HotkeysModal.tsx index 0bd7be796d..be0e5ec2b7 100644 --- a/frontend/src/features/system/components/HotkeysModal/HotkeysModal.tsx +++ b/frontend/src/features/system/components/HotkeysModal/HotkeysModal.tsx @@ -178,14 +178,19 @@ export default function HotkeysModal({ children }: HotkeysModalProps) { desc: 'Selects the canvas color picker', hotkey: 'C', }, + { + title: 'Toggle Snap', + desc: 'Toggles Snap to Grid', + hotkey: 'N', + }, { title: 'Quick Toggle Move', desc: 'Temporarily toggles Move mode', hotkey: 'Hold Space', }, { - title: 'Select Mask Layer', - desc: 'Toggles mask layer', + title: 'Toggle Layer', + desc: 'Toggles mask/base layer selection', hotkey: 'Q', }, { diff --git a/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx b/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx index 52afd2e187..e600f33509 100644 --- a/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx +++ b/frontend/src/features/tabs/components/UnifiedCanvas/UnifiedCanvasPanel.tsx @@ -1,8 +1,7 @@ // import { Feature } from 'app/features'; import { Feature } from 'app/features'; import ImageToImageStrength from 'features/options/components/AdvancedOptions/ImageToImage/ImageToImageStrength'; -import BoundingBoxSettings from 'features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings'; -import CompositionOptions from 'features/options/components/AdvancedOptions/Canvas/CompositionOptions'; +import SeamCorrectionOptions from 'features/options/components/AdvancedOptions/Canvas/SeamCorrectionOptions'; import SeedOptions from 'features/options/components/AdvancedOptions/Seed/SeedOptions'; import GenerateVariationsToggle from 'features/options/components/AdvancedOptions/Variations/GenerateVariations'; import VariationsOptions from 'features/options/components/AdvancedOptions/Variations/VariationsOptions'; @@ -11,6 +10,8 @@ import OptionsAccordion from 'features/options/components/OptionsAccordion'; import ProcessButtons from 'features/options/components/ProcessButtons/ProcessButtons'; import PromptInput from 'features/options/components/PromptInput/PromptInput'; import InvokeOptionsPanel from 'features/tabs/components/InvokeOptionsPanel'; +import BoundingBoxSettings from 'features/options/components/AdvancedOptions/Canvas/BoundingBoxSettings/BoundingBoxSettings'; +import InfillAndScalingOptions from 'features/options/components/AdvancedOptions/Canvas/InfillAndScalingOptions'; export default function UnifiedCanvasPanel() { const unifiedCanvasAccordions = { @@ -19,10 +20,15 @@ export default function UnifiedCanvasPanel() { feature: Feature.BOUNDING_BOX, content: , }, - composition: { - header: 'Composition', - feature: Feature.CANVAS_COMPOSITION, - content: , + seamCorrection: { + header: 'Seam Correction', + feature: Feature.SEAM_CORRECTION, + content: , + }, + infillAndScaling: { + header: 'Infill and Scaling', + feature: Feature.INFILL_AND_SCALING, + content: , }, seed: { header: 'Seed',