From 5e20c9a1ca4189b5205782cb7c331dab8e30ebf7 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 13 Apr 2025 23:36:51 -0400 Subject: [PATCH] mask noise slider option --- invokeai/frontend/web/public/locales/en.json | 2 + .../components/InpaintMask/InpaintMask.tsx | 2 + .../InpaintMask/InpaintMaskAddNoiseButton.tsx | 19 ++++++ .../InpaintMaskDeleteNoiseButton.tsx | 29 ++++++++ .../InpaintMask/InpaintMaskMenuItems.tsx | 3 + .../InpaintMaskMenuItemsAddNoise.tsx | 21 ++++++ .../InpaintMask/InpaintMaskNoiseSlider.tsx | 68 +++++++++++++++++++ .../InpaintMask/InpaintMaskSettings.tsx | 32 +++++++++ .../controlLayers/hooks/addLayerHooks.ts | 10 +++ .../controlLayers/store/canvasSlice.ts | 27 ++++++++ .../src/features/controlLayers/store/types.ts | 1 + .../src/features/controlLayers/store/util.ts | 1 + 12 files changed, 215 insertions(+) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDeleteNoiseButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d7ac0a809e..97e2544362 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1907,6 +1907,7 @@ "addPositivePrompt": "Add $t(controlLayers.prompt)", "addNegativePrompt": "Add $t(controlLayers.negativePrompt)", "addReferenceImage": "Add $t(controlLayers.referenceImage)", + "addImageNoise": "Add $t(controlLayers.imageNoise)", "addRasterLayer": "Add $t(controlLayers.rasterLayer)", "addControlLayer": "Add $t(controlLayers.controlLayer)", "addInpaintMask": "Add $t(controlLayers.inpaintMask)", @@ -2011,6 +2012,7 @@ "replaceCurrent": "Replace Current", "controlLayerEmptyState": "Upload an image, drag an image from the gallery onto this layer, pull the bounding box into this layer, or draw on the canvas to get started.", "referenceImageEmptyState": "Upload an image, drag an image from the gallery onto this layer, or pull the bounding box into this layer to get started.", + "imageNoise": "Image Noise", "warnings": { "problemsFound": "Problems found", "unsupportedModel": "layer not supported for selected base model", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index ecfe91c33d..fc600018cc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -4,6 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions'; import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage'; import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit'; +import { InpaintMaskSettings } from 'features/controlLayers/components/InpaintMask/InpaintMaskSettings'; import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate'; import { InpaintMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; @@ -28,6 +29,7 @@ export const InpaintMask = memo(({ id }: Props) => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton.tsx new file mode 100644 index 0000000000..2ad235b85a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton.tsx @@ -0,0 +1,19 @@ +import { Button, Flex } from '@invoke-ai/ui-library'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useAddInpaintMaskNoise } from 'features/controlLayers/hooks/addLayerHooks'; +import { useTranslation } from 'react-i18next'; +import { PiPlusBold } from 'react-icons/pi'; + +export const InpaintMaskAddNoiseButton = () => { + const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); + const { t } = useTranslation(); + const addInpaintMaskNoise = useAddInpaintMaskNoise(entityIdentifier); + + return ( + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDeleteNoiseButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDeleteNoiseButton.tsx new file mode 100644 index 0000000000..17b6120e0e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDeleteNoiseButton.tsx @@ -0,0 +1,29 @@ +import type { IconButtonProps } from '@invoke-ai/ui-library'; +import { IconButton } from '@invoke-ai/ui-library'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiXBold } from 'react-icons/pi'; + +type Props = Omit & { + onDelete: () => void; +}; + +export const InpaintMaskDeleteNoiseButton = memo(({ onDelete, ...rest }: Props) => { + const { t } = useTranslation(); + return ( + } + onClick={onDelete} + flexGrow={0} + size="sm" + p={0} + colorScheme="error" + {...rest} + /> + ); +}); + +InpaintMaskDeleteNoiseButton.displayName = 'InpaintMaskDeleteNoiseButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx index ae99388bd7..179c9acc26 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx @@ -7,6 +7,7 @@ import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/component import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown'; import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; +import { InpaintMaskMenuItemsAddNoise } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise'; import { InpaintMaskMenuItemsConvertToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsConvertToSubMenu'; import { InpaintMaskMenuItemsCopyToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsCopyToSubMenu'; import { memo } from 'react'; @@ -20,6 +21,8 @@ export const InpaintMaskMenuItems = memo(() => { + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise.tsx new file mode 100644 index 0000000000..ad8bea2c29 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddNoise.tsx @@ -0,0 +1,21 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useAddInpaintMaskNoise } from 'features/controlLayers/hooks/addLayerHooks'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const InpaintMaskMenuItemsAddNoise = memo(() => { + const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); + const { t } = useTranslation(); + const isBusy = useCanvasIsBusy(); + const addInpaintMaskNoise = useAddInpaintMaskNoise(entityIdentifier); + + return ( + + {t('controlLayers.addImageNoise')} + + ); +}); + +InpaintMaskMenuItemsAddNoise.displayName = 'InpaintMaskMenuItemsAddNoise'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx new file mode 100644 index 0000000000..8b206ceb28 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx @@ -0,0 +1,68 @@ +import { Flex, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { inpaintMaskNoiseChanged, inpaintMaskNoiseDeleted } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { InpaintMaskDeleteNoiseButton } from './InpaintMaskDeleteNoiseButton'; + +export const InpaintMaskNoiseSlider = memo(() => { + const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const selectNoiseLevel = useMemo( + () => + createSelector( + selectCanvasSlice, + (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskNoiseSlider').noiseLevel + ), + [entityIdentifier] + ); + const noiseLevel = useAppSelector(selectNoiseLevel); + + const handleNoiseChange = useCallback( + (value: number) => { + dispatch(inpaintMaskNoiseChanged({ entityIdentifier, noiseLevel: value })); + }, + [dispatch, entityIdentifier] + ); + + const onDeleteNoise = useCallback(() => { + dispatch(inpaintMaskNoiseDeleted({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + if (noiseLevel === null) { + return null; + } + + return ( + + + {t('controlLayers.imageNoise')} + + {Math.round(noiseLevel * 100)}% + + + + + + + + + + + ); +}); + +InpaintMaskNoiseSlider.displayName = 'InpaintMaskNoiseSlider'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx new file mode 100644 index 0000000000..a3d378ae23 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx @@ -0,0 +1,32 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; +import { InpaintMaskAddNoiseButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskAddNoiseButton'; +import { InpaintMaskNoiseSlider } from 'features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; + +const buildSelectFlags = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => + createMemoizedSelector(selectCanvasSlice, (canvas) => { + const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings'); + return { + hasNoiseLevel: entity.noiseLevel !== null, + }; + }); + +export const InpaintMaskSettings = memo(() => { + const entityIdentifier = useEntityIdentifierContext('inpaint_mask'); + const selectFlags = useMemo(() => buildSelectFlags(entityIdentifier), [entityIdentifier]); + const flags = useAppSelector(selectFlags); + + return ( + + {!flags.hasNoiseLevel && } + {flags.hasNoiseLevel && } + + ); +}); + +InpaintMaskSettings.displayName = 'InpaintMaskSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 27b01fe494..dc038bc664 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -6,6 +6,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import { controlLayerAdded, inpaintMaskAdded, + inpaintMaskNoiseAdded, rasterLayerAdded, referenceImageAdded, rgAdded, @@ -222,6 +223,15 @@ export const useAddRegionalGuidanceNegativePrompt = (entityIdentifier: CanvasEnt return runc; }; +export const useAddInpaintMaskNoise = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => { + const dispatch = useAppDispatch(); + const func = useCallback(() => { + dispatch(inpaintMaskNoiseAdded({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return func; +}; + export const buildSelectValidRegionalGuidanceActions = ( entityIdentifier: CanvasEntityIdentifier<'regional_guidance'> ) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 8a2b866e56..b1348395fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1096,6 +1096,30 @@ export const canvasSlice = createSlice({ state.inpaintMasks.entities = [data]; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, + inpaintMaskNoiseAdded: (state, action: PayloadAction>) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (entity && entity.type === 'inpaint_mask') { + entity.noiseLevel = 0.1; // Default noise level + } + }, + inpaintMaskNoiseChanged: ( + state, + action: PayloadAction> + ) => { + const { entityIdentifier, noiseLevel } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (entity && entity.type === 'inpaint_mask') { + entity.noiseLevel = noiseLevel; + } + }, + inpaintMaskNoiseDeleted: (state, action: PayloadAction>) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (entity && entity.type === 'inpaint_mask') { + entity.noiseLevel = null; + } + }, inpaintMaskConvertedToRegionalGuidance: { reducer: ( state, @@ -1869,6 +1893,9 @@ export const { // Inpaint mask inpaintMaskAdded, inpaintMaskConvertedToRegionalGuidance, + inpaintMaskNoiseAdded, + inpaintMaskNoiseChanged, + inpaintMaskNoiseDeleted, // inpaintMaskRecalled, } = canvasSlice.actions; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 268e6004dd..4b211fd6cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -310,6 +310,7 @@ const zCanvasInpaintMaskState = zCanvasEntityBase.extend({ fill: zFill, opacity: zOpacity, objects: z.array(zCanvasObjectState), + noiseLevel: z.number().gte(0).lte(1).nullable().default(null), }); export type CanvasInpaintMaskState = z.infer; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/util.ts b/invokeai/frontend/web/src/features/controlLayers/store/util.ts index c5d23b8c08..4acf92228c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts @@ -199,6 +199,7 @@ export const getInpaintMaskState = ( style: 'diagonal', color: getInpaintMaskFillColor(), }, + noiseLevel: null, }; merge(entityState, overrides); return entityState;