mask noise slider option

This commit is contained in:
dunkeroni
2025-04-13 23:36:51 -04:00
committed by psychedelicious
parent 933cf5f276
commit 5e20c9a1ca
12 changed files with 215 additions and 0 deletions

View File

@@ -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": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, <PullBboxButton>pull the bounding box into this layer</PullBboxButton>, or draw on the canvas to get started.",
"referenceImageEmptyState": "<UploadButton>Upload an image</UploadButton>, drag an image from the <GalleryButton>gallery</GalleryButton> onto this layer, or <PullBboxButton>pull the bounding box into this layer</PullBboxButton> to get started.",
"imageNoise": "Image Noise",
"warnings": {
"problemsFound": "Problems found",
"unsupportedModel": "layer not supported for selected base model",

View File

@@ -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) => {
<Spacer />
<CanvasEntityHeaderCommonActions />
</CanvasEntityHeader>
<InpaintMaskSettings />
</CanvasEntityContainer>
</CanvasEntityStateGate>
</InpaintMaskAdapterGate>

View File

@@ -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 (
<Flex w="full" p={2} justifyContent="center">
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addInpaintMaskNoise}>
{t('controlLayers.imageNoise')}
</Button>
</Flex>
);
};

View File

@@ -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<IconButtonProps, 'aria-label'> & {
onDelete: () => void;
};
export const InpaintMaskDeleteNoiseButton = memo(({ onDelete, ...rest }: Props) => {
const { t } = useTranslation();
return (
<IconButton
tooltip={t('common.delete')}
variant="link"
aria-label={t('common.delete')}
icon={<PiXBold />}
onClick={onDelete}
flexGrow={0}
size="sm"
p={0}
colorScheme="error"
{...rest}
/>
);
});
InpaintMaskDeleteNoiseButton.displayName = 'InpaintMaskDeleteNoiseButton';

View File

@@ -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(() => {
<CanvasEntityMenuItemsDelete asIcon />
</IconMenuItemGroup>
<MenuDivider />
<InpaintMaskMenuItemsAddNoise />
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />

View File

@@ -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 (
<MenuItem onClick={addInpaintMaskNoise} isDisabled={isBusy}>
{t('controlLayers.addImageNoise')}
</MenuItem>
);
});
InpaintMaskMenuItemsAddNoise.displayName = 'InpaintMaskMenuItemsAddNoise';

View File

@@ -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 (
<Flex direction="column" gap={1} w="full" px={2} pb={2}>
<Flex justifyContent="space-between" w="full" alignItems="center">
<Text fontSize="sm">{t('controlLayers.imageNoise')}</Text>
<Flex alignItems="center" gap={1}>
<Text fontSize="sm">{Math.round(noiseLevel * 100)}%</Text>
<InpaintMaskDeleteNoiseButton onDelete={onDeleteNoise} />
</Flex>
</Flex>
<Slider
aria-label={t('controlLayers.imageNoise')}
value={noiseLevel}
min={0}
max={1}
step={0.01}
onChange={handleNoiseChange}
>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
);
});
InpaintMaskNoiseSlider.displayName = 'InpaintMaskNoiseSlider';

View File

@@ -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 (
<CanvasEntitySettingsWrapper>
{!flags.hasNoiseLevel && <InpaintMaskAddNoiseButton />}
{flags.hasNoiseLevel && <InpaintMaskNoiseSlider />}
</CanvasEntitySettingsWrapper>
);
});
InpaintMaskSettings.displayName = 'InpaintMaskSettings';

View File

@@ -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'>
) => {

View File

@@ -1096,6 +1096,30 @@ export const canvasSlice = createSlice({
state.inpaintMasks.entities = [data];
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
},
inpaintMaskNoiseAdded: (state, action: PayloadAction<EntityIdentifierPayload<void, 'inpaint_mask'>>) => {
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<EntityIdentifierPayload<{ noiseLevel: number }, 'inpaint_mask'>>
) => {
const { entityIdentifier, noiseLevel } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (entity && entity.type === 'inpaint_mask') {
entity.noiseLevel = noiseLevel;
}
},
inpaintMaskNoiseDeleted: (state, action: PayloadAction<EntityIdentifierPayload<void, 'inpaint_mask'>>) => {
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;

View File

@@ -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<typeof zCanvasInpaintMaskState>;

View File

@@ -199,6 +199,7 @@ export const getInpaintMaskState = (
style: 'diagonal',
color: getInpaintMaskFillColor(),
},
noiseLevel: null,
};
merge(entityState, overrides);
return entityState;