diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 9d9088f75c..4f139dda31 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -6,6 +6,7 @@ import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; +import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; import { lorasReducers } from 'features/controlLayers/store/lorasReducers'; @@ -29,17 +30,14 @@ const initialState: CanvasV2State = { regions: { entities: [] }, loras: [], inpaintMask: { + id: 'inpaint_mask', + type: 'inpaint_mask', bbox: null, bboxNeedsUpdate: false, - fill: { - type: 'color_fill', - color: DEFAULT_RGBA_COLOR, - }, - id: 'inpaint_mask', + fill: DEFAULT_RGBA_COLOR, imageCache: null, isEnabled: false, - maskObjects: [], - type: 'inpaint_mask', + objects: [], x: 0, y: 0, }, @@ -135,6 +133,7 @@ export const canvasV2Slice = createSlice({ ...settingsReducers, ...toolReducers, ...bboxReducers, + ...inpaintMaskReducers, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; state.document.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; @@ -314,6 +313,17 @@ export const { loraWeightChanged, loraIsEnabledChanged, loraAllDeleted, + // Inpaint mask + imReset, + imRecalled, + imIsEnabledToggled, + imTranslated, + imBboxChanged, + imImageCacheChanged, + imBrushLineAdded, + imEraserLineAdded, + imLinePointAdded, + imRectAdded, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 8b13789179..bff4a8a9bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1 +1,108 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { CanvasV2State, InpaintMaskEntity } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import type { IRect } from 'konva/lib/types'; +import type { ImageDTO } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; +import type { BrushLineAddedArg, EraserLineAddedArg, PointAddedToLineArg, RectShapeAddedArg, RgbColor } from './types'; +import { isLine } from './types'; + +export const inpaintMaskReducers = { + imReset: (state) => { + state.inpaintMask.objects = []; + state.inpaintMask.bbox = null; + state.inpaintMask.bboxNeedsUpdate = false; + state.inpaintMask.imageCache = null; + }, + imRecalled: (state, action: PayloadAction<{ data: InpaintMaskEntity }>) => { + const { data } = action.payload; + state.inpaintMask = data; + state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; + }, + imIsEnabledToggled: (state) => { + state.inpaintMask.isEnabled = !state.inpaintMask.isEnabled; + }, + imTranslated: (state, action: PayloadAction<{ x: number; y: number }>) => { + const { x, y } = action.payload; + state.inpaintMask.x = x; + state.inpaintMask.y = y; + }, + imBboxChanged: (state, action: PayloadAction<{ bbox: IRect | null }>) => { + const { bbox } = action.payload; + state.inpaintMask.bbox = bbox; + state.inpaintMask.bboxNeedsUpdate = false; + }, + inpaintMaskFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { + const { fill } = action.payload; + state.inpaintMask.fill = fill; + }, + imImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { + const { imageDTO } = action.payload; + state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + imBrushLineAdded: { + reducer: (state, action: PayloadAction & { lineId: string }>) => { + const { points, lineId, color, width, clip } = action.payload; + state.inpaintMask.objects.push({ + id: getBrushLineId(state.inpaintMask.id, lineId), + type: 'brush_line', + points, + strokeWidth: width, + color, + clip, + }); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + prepare: (payload: Omit) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + imEraserLineAdded: { + reducer: (state, action: PayloadAction & { lineId: string }>) => { + const { points, lineId, width, clip } = action.payload; + state.inpaintMask.objects.push({ + id: getEraserLineId(state.inpaintMask.id, lineId), + type: 'eraser_line', + points, + strokeWidth: width, + clip, + }); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + prepare: (payload: Omit) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + imLinePointAdded: (state, action: PayloadAction>) => { + const { point } = action.payload; + const lastObject = state.inpaintMask.objects[state.inpaintMask.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + lastObject.points.push(...point); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + imRectAdded: { + reducer: (state, action: PayloadAction & { rectId: string }>) => { + const { rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + state.inpaintMask.objects.push({ + type: 'rect_shape', + id: getRectShapeId(state.inpaintMask.id, rectId), + ...rect, + color, + }); + state.inpaintMask.bboxNeedsUpdate = true; + state.inpaintMask.imageCache = null; + }, + prepare: (payload: Omit) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 44ea8cb500..f8410bb4b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -655,8 +655,8 @@ const zInpaintMaskEntity = z.object({ y: z.number(), bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), - maskObjects: z.array(zMaskObject), - fill: zFill, + objects: z.array(zMaskObject), + fill: zRgbColor, imageCache: zImageWithDims.nullable(), }); export type InpaintMaskEntity = z.infer;