Skip to content

Commit a309d04

Browse files
Add invert mask functionality to inpainting control layer
Co-authored-by: kent <kent@invoke.ai>
1 parent d0619c0 commit a309d04

File tree

4 files changed

+70
-0
lines changed

4 files changed

+70
-0
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,7 @@
20742074
"uploadOrDragAnImage": "Drag an image from the gallery or <UploadButton>upload an image</UploadButton>.",
20752075
"imageNoise": "Image Noise",
20762076
"denoiseLimit": "Denoise Limit",
2077+
"invertMask": "Invert Mask",
20772078
"warnings": {
20782079
"problemsFound": "Problems found",
20792080
"unsupportedModel": "layer not supported for selected base model",

invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/component
1010
import { InpaintMaskMenuItemsAddModifiers } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsAddModifiers';
1111
import { InpaintMaskMenuItemsConvertToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsConvertToSubMenu';
1212
import { InpaintMaskMenuItemsCopyToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsCopyToSubMenu';
13+
import { InpaintMaskMenuItemsInvert } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsInvert';
1314
import { memo } from 'react';
1415

1516
export const InpaintMaskMenuItems = memo(() => {
@@ -21,6 +22,7 @@ export const InpaintMaskMenuItems = memo(() => {
2122
<CanvasEntityMenuItemsDelete asIcon />
2223
</IconMenuItemGroup>
2324
<MenuDivider />
25+
<InpaintMaskMenuItemsInvert />
2426
<InpaintMaskMenuItemsAddModifiers />
2527
<MenuDivider />
2628
<CanvasEntityMenuItemsTransform />
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { MenuItem } from '@invoke-ai/ui-library';
2+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
3+
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
4+
import { inpaintMaskInverted } from 'features/controlLayers/store/canvasSlice';
5+
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
6+
import { memo, useCallback } from 'react';
7+
import { useTranslation } from 'react-i18next';
8+
import { PiSelectionInverseBold } from 'react-icons/pi';
9+
10+
export const InpaintMaskMenuItemsInvert = memo(() => {
11+
const { t } = useTranslation();
12+
const dispatch = useAppDispatch();
13+
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
14+
const canvasSlice = useAppSelector(selectCanvasSlice);
15+
16+
const handleInvertMask = useCallback(() => {
17+
dispatch(inpaintMaskInverted({ entityIdentifier }));
18+
}, [dispatch, entityIdentifier]);
19+
20+
// Only show if there are objects to invert and we have a valid bounding box
21+
const entity = canvasSlice.inpaintMasks.entities.find((entity) => entity.id === entityIdentifier.id);
22+
const hasObjects = entity?.objects.length > 0;
23+
const hasBbox = canvasSlice.bbox.rect.width > 0 && canvasSlice.bbox.rect.height > 0;
24+
25+
if (!hasObjects || !hasBbox) {
26+
return null;
27+
}
28+
29+
return (
30+
<MenuItem onClick={handleInvertMask} icon={<PiSelectionInverseBold />}>
31+
{t('controlLayers.invertMask')}
32+
</MenuItem>
33+
);
34+
});
35+
36+
InpaintMaskMenuItemsInvert.displayName = 'InpaintMaskMenuItemsInvert';

invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,36 @@ export const canvasSlice = createSlice({
993993
entity.denoiseLimit = undefined;
994994
}
995995
},
996+
inpaintMaskInverted: (state, action: PayloadAction<EntityIdentifierPayload<void, 'inpaint_mask'>>) => {
997+
const { entityIdentifier } = action.payload;
998+
const entity = selectEntity(state, entityIdentifier);
999+
if (!entity || entity.type !== 'inpaint_mask') {
1000+
return;
1001+
}
1002+
1003+
// Create a rectangle covering the current bounding box
1004+
const bboxRect = state.bbox.rect;
1005+
const fillRectObject = {
1006+
id: getPrefixedId('rect'),
1007+
type: 'rect' as const,
1008+
rect: {
1009+
x: bboxRect.x - entity.position.x,
1010+
y: bboxRect.y - entity.position.y,
1011+
width: bboxRect.width,
1012+
height: bboxRect.height,
1013+
},
1014+
color: { r: 255, g: 255, b: 255, a: 1 },
1015+
};
1016+
1017+
// Convert existing objects to eraser effect by creating a composite inverted mask
1018+
// The strategy is to replace all existing objects with:
1019+
// 1. A full rectangle covering the bbox
1020+
// 2. The original objects as "erasers" to punch holes through the rectangle
1021+
const originalObjects = [...entity.objects];
1022+
1023+
// Start with the full rectangle, then "erase" the original painted areas
1024+
entity.objects = [fillRectObject, ...originalObjects];
1025+
},
9961026
//#region BBox
9971027
bboxScaledWidthChanged: (state, action: PayloadAction<number>) => {
9981028
const gridSize = getGridSize(state.bbox.modelBase);
@@ -1713,6 +1743,7 @@ export const {
17131743
inpaintMaskDenoiseLimitAdded,
17141744
inpaintMaskDenoiseLimitChanged,
17151745
inpaintMaskDenoiseLimitDeleted,
1746+
inpaintMaskInverted,
17161747
// inpaintMaskRecalled,
17171748
} = canvasSlice.actions;
17181749

0 commit comments

Comments
 (0)