diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 77fe9be9b2..1d610d32c2 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -182,7 +182,7 @@ const createSelector = (templates: Templates) => if (l.type === 'regional_guidance_layer') { // Must have a region - if (l.maskObjects.length === 0) { + if (l.objects.length === 0) { problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); } // Must have at least 1 prompt or IP Adapter diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx new file mode 100644 index 0000000000..517385f0d3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx @@ -0,0 +1,48 @@ +import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIColorPicker from 'common/components/IAIColorPicker'; +import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { brushColorChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { RgbaColor } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const BrushColorPicker = memo(() => { + const { t } = useTranslation(); + const brushColor = useAppSelector((s) => s.controlLayers.present.brushColor); + const dispatch = useAppDispatch(); + const onChange = useCallback( + (color: RgbaColor) => { + dispatch(brushColorChanged(color)); + }, + [dispatch] + ); + return ( + + + + + + + + + + + + + + + ); +}); + +BrushColorPicker.displayName = 'BrushColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 8cc3aa93fe..55025d40f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -1,5 +1,6 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; +import { BrushColorPicker } from 'features/controlLayers/components/BrushColorPicker'; import { BrushSize } from 'features/controlLayers/components/BrushSize'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; @@ -18,6 +19,7 @@ export const ControlLayersToolbar = memo(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 9226abf207..a4dc52751e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -8,26 +8,32 @@ import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'f import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers'; import { + $brushColor, $brushSize, $brushSpacingPx, $isDrawing, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, - $selectedLayerId, - $selectedLayerType, + $selectedLayer, $shouldInvertBrushSizeScrollDirection, $tool, + brushLineAdded, brushSizeChanged, + eraserLineAdded, isRegionalGuidanceLayer, layerBboxChanged, layerTranslated, - rgLayerLineAdded, - rgLayerPointsAdded, - rgLayerRectAdded, + linePointsAdded, + rectAdded, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; -import type { AddLineArg, AddPointToLineArg, AddRectArg } from 'features/controlLayers/store/types'; +import type { + AddBrushLineArg, + AddEraserLineArg, + AddPointToLineArg, + AddRectShapeArg, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { clamp } from 'lodash-es'; @@ -41,16 +47,20 @@ Konva.showWarnings = false; const log = logger('controlLayers'); -const selectSelectedLayerColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { +const selectBrushColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const layer = controlLayers.present.layers .filter(isRegionalGuidanceLayer) .find((l) => l.id === controlLayers.present.selectedLayerId); - return layer?.previewColor ?? null; + + if (layer) { + return { ...layer.previewColor, a: controlLayers.present.globalMaskLayerOpacity }; + } + + return controlLayers.present.brushColor; }); -const selectSelectedLayerType = createSelector(selectControlLayersSlice, (controlLayers) => { - const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); - return selectedLayer?.type ?? null; +const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLayers) => { + return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null; }); const useStageRenderer = ( @@ -64,8 +74,8 @@ const useStageRenderer = ( const tool = useStore($tool); const lastCursorPos = useStore($lastCursorPos); const lastMouseDownPos = useStore($lastMouseDownPos); - const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); - const selectedLayerType = useAppSelector(selectSelectedLayerType); + const brushColor = useAppSelector(selectBrushColor); + const selectedLayer = useAppSelector(selectSelectedLayer); const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]); const layerCount = useMemo(() => state.layers.length, [state.layers]); const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); @@ -77,18 +87,19 @@ const useStageRenderer = ( ); useLayoutEffect(() => { + $brushColor.set(brushColor); $brushSize.set(state.brushSize); $brushSpacingPx.set(brushSpacingPx); - $selectedLayerId.set(state.selectedLayerId); - $selectedLayerType.set(selectedLayerType); + $selectedLayer.set(selectedLayer); $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); }, [ brushSpacingPx, - selectedLayerIdColor, - selectedLayerType, + brushColor, + selectedLayer, shouldInvertBrushSizeScrollDirection, state.brushSize, state.selectedLayerId, + state.brushColor, ]); const onLayerPosChanged = useCallback( @@ -105,21 +116,27 @@ const useStageRenderer = ( [dispatch] ); - const onRGLayerLineAdded = useCallback( - (arg: AddLineArg) => { - dispatch(rgLayerLineAdded(arg)); + const onBrushLineAdded = useCallback( + (arg: AddBrushLineArg) => { + dispatch(brushLineAdded(arg)); }, [dispatch] ); - const onRGLayerPointAddedToLine = useCallback( + const onEraserLineAdded = useCallback( + (arg: AddEraserLineArg) => { + dispatch(eraserLineAdded(arg)); + }, + [dispatch] + ); + const onPointAddedToLine = useCallback( (arg: AddPointToLineArg) => { - dispatch(rgLayerPointsAdded(arg)); + dispatch(linePointsAdded(arg)); }, [dispatch] ); - const onRGLayerRectAdded = useCallback( - (arg: AddRectArg) => { - dispatch(rgLayerRectAdded(arg)); + const onRectShapeAdded = useCallback( + (arg: AddRectShapeArg) => { + dispatch(rectAdded(arg)); }, [dispatch] ); @@ -155,21 +172,22 @@ const useStageRenderer = ( $lastCursorPos, $lastAddedPoint, $brushSize, + $brushColor, $brushSpacingPx, - $selectedLayerId, - $selectedLayerType, + $selectedLayer, $shouldInvertBrushSizeScrollDirection, - onRGLayerLineAdded, - onRGLayerPointAddedToLine, - onRGLayerRectAdded, onBrushSizeChanged, + onBrushLineAdded, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, }); return () => { log.trace('Removing stage listeners'); cleanup(); }; - }, [asPreview, onBrushSizeChanged, onRGLayerLineAdded, onRGLayerPointAddedToLine, onRGLayerRectAdded, stage]); + }, [asPreview, onBrushLineAdded, onBrushSizeChanged, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, stage]); useLayoutEffect(() => { log.trace('Updating stage dimensions'); @@ -205,8 +223,8 @@ const useStageRenderer = ( renderers.renderToolPreview( stage, tool, - selectedLayerIdColor, - selectedLayerType, + brushColor, + selectedLayer?.type ?? null, state.globalMaskLayerOpacity, lastCursorPos, lastMouseDownPos, @@ -216,8 +234,8 @@ const useStageRenderer = ( asPreview, stage, tool, - selectedLayerIdColor, - selectedLayerType, + brushColor, + selectedLayer, state.globalMaskLayerOpacity, lastCursorPos, lastMouseDownPos, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index f97a0f35e5..b9ea0af459 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -15,7 +15,7 @@ import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBol const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => { const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); - return selectedLayer?.type !== 'regional_guidance_layer'; + return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; }); export const ToolChooser: React.FC = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 8b130e940f..0a26dba92d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -5,10 +5,19 @@ import { getScaledFlooredCursorPosition, snapPosToStage, } from 'features/controlLayers/konva/util'; -import type { AddLineArg, AddPointToLineArg, AddRectArg, Layer, Tool } from 'features/controlLayers/store/types'; +import { + type AddBrushLineArg, + type AddEraserLineArg, + type AddPointToLineArg, + type AddRectShapeArg, + DEFAULT_RGBA_COLOR, + type Layer, + type Tool, +} from 'features/controlLayers/store/types'; import type Konva from 'konva'; import type { Vector2d } from 'konva/lib/types'; import type { WritableAtom } from 'nanostores'; +import type { RgbaColor } from 'react-colorful'; import { TOOL_PREVIEW_LAYER_ID } from './naming'; @@ -19,14 +28,15 @@ type SetStageEventHandlersArg = { $lastMouseDownPos: WritableAtom; $lastCursorPos: WritableAtom; $lastAddedPoint: WritableAtom; + $brushColor: WritableAtom; $brushSize: WritableAtom; $brushSpacingPx: WritableAtom; - $selectedLayerId: WritableAtom; - $selectedLayerType: WritableAtom; + $selectedLayer: WritableAtom; $shouldInvertBrushSizeScrollDirection: WritableAtom; - onRGLayerLineAdded: (arg: AddLineArg) => void; - onRGLayerPointAddedToLine: (arg: AddPointToLineArg) => void; - onRGLayerRectAdded: (arg: AddRectArg) => void; + onBrushLineAdded: (arg: AddBrushLineArg) => void; + onEraserLineAdded: (arg: AddEraserLineArg) => void; + onPointAddedToLine: (arg: AddPointToLineArg) => void; + onRectShapeAdded: (arg: AddRectShapeArg) => void; onBrushSizeChanged: (size: number) => void; }; @@ -46,14 +56,15 @@ export const setStageEventHandlers = ({ $lastMouseDownPos, $lastCursorPos, $lastAddedPoint, + $brushColor, $brushSize, $brushSpacingPx, - $selectedLayerId, - $selectedLayerType, + $selectedLayer, $shouldInvertBrushSizeScrollDirection, - onRGLayerLineAdded, - onRGLayerPointAddedToLine, - onRGLayerRectAdded, + onBrushLineAdded, + onEraserLineAdded, + onPointAddedToLine, + onRectShapeAdded, onBrushSizeChanged, }: SetStageEventHandlersArg): (() => void) => { stage.on('mouseenter', (e) => { @@ -72,16 +83,25 @@ export const setStageEventHandlers = ({ } const tool = $tool.get(); const pos = syncCursorPos(stage, $lastCursorPos); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + const selectedLayer = $selectedLayer.get(); + if (!pos || !selectedLayer) { return; } - if (tool === 'brush' || tool === 'eraser') { - onRGLayerLineAdded({ - layerId: selectedLayerId, + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { + return; + } + if (tool === 'brush') { + onBrushLineAdded({ + layerId: selectedLayer.id, + points: [pos.x, pos.y, pos.x, pos.y], + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + }); + $isDrawing.set(true); + $lastMouseDownPos.set(pos); + } else if (tool === 'eraser') { + onEraserLineAdded({ + layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y], - tool, }); $isDrawing.set(true); $lastMouseDownPos.set(pos); @@ -96,24 +116,27 @@ export const setStageEventHandlers = ({ return; } const pos = $lastCursorPos.get(); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); + const selectedLayer = $selectedLayer.get(); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + if (!pos || !selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } const lastPos = $lastMouseDownPos.get(); const tool = $tool.get(); - if (lastPos && selectedLayerId && tool === 'rect') { + if (lastPos && selectedLayer.id && tool === 'rect') { const snappedPos = snapPosToStage(pos, stage); - onRGLayerRectAdded({ - layerId: selectedLayerId, + onRectShapeAdded({ + layerId: selectedLayer.id, rect: { x: Math.min(snappedPos.x, lastPos.x), y: Math.min(snappedPos.y, lastPos.y), width: Math.abs(snappedPos.x - lastPos.x), height: Math.abs(snappedPos.y - lastPos.y), }, + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, }); } $isDrawing.set(false); @@ -127,12 +150,14 @@ export const setStageEventHandlers = ({ } const tool = $tool.get(); const pos = syncCursorPos(stage, $lastCursorPos); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); + const selectedLayer = $selectedLayer.get(); stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + if (!pos || !selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { @@ -146,10 +171,21 @@ export const setStageEventHandlers = ({ } } $lastAddedPoint.set({ x: pos.x, y: pos.y }); - onRGLayerPointAddedToLine({ layerId: selectedLayerId, point: [pos.x, pos.y] }); + onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); } else { - // Start a new line - onRGLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool }); + if (tool === 'brush') { + // Start a new line + onBrushLineAdded({ + layerId: selectedLayer.id, + points: [pos.x, pos.y, pos.x, pos.y], + color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, + }); + } else if (tool === 'eraser') { + onEraserLineAdded({ + layerId: selectedLayer.id, + points: [pos.x, pos.y, pos.x, pos.y], + }); + } } $isDrawing.set(true); } @@ -164,28 +200,36 @@ export const setStageEventHandlers = ({ $isDrawing.set(false); $lastCursorPos.set(null); $lastMouseDownPos.set(null); - const selectedLayerId = $selectedLayerId.get(); - const selectedLayerType = $selectedLayerType.get(); + const selectedLayer = $selectedLayer.get(); const tool = $tool.get(); stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false); - if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + if (!pos || !selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { - onRGLayerPointAddedToLine({ layerId: selectedLayerId, point: [pos.x, pos.y] }); + onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); } }); stage.on('wheel', (e) => { e.evt.preventDefault(); - const selectedLayerType = $selectedLayerType.get(); const tool = $tool.get(); - if (selectedLayerType !== 'regional_guidance_layer' || (tool !== 'brush' && tool !== 'eraser')) { + const selectedLayer = $selectedLayer.get(); + + if (tool !== 'brush' && tool !== 'eraser') { + return; + } + if (!selectedLayer) { + return; + } + if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { return; } - // Invert the delta if the property is set to true let delta = e.evt.deltaY; if ($shouldInvertBrushSizeScrollDirection.get()) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 3a338b41a0..f8175c9655 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -26,13 +26,17 @@ export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image'; export const LAYER_BBOX_NAME = 'layer.bbox'; export const COMPOSITING_RECT_NAME = 'compositing-rect'; export const RASTER_LAYER_NAME = 'raster_layer'; +export const RASTER_LAYER_LINE_NAME = 'raster_layer.line'; +export const RASTER_LAYER_OBJECT_GROUP_NAME = 'raster_layer.object_group'; +export const RASTER_LAYER_RECT_NAME = 'raster_layer.rect'; // Getters for non-singleton layer and object IDs export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`; -export const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; -export const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; -export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; +export const getBrushLineId = (layerId: string, lineId: string) => `${layerId}.brush_line_${lineId}`; +export const getEraserLineId = (layerId: string, lineId: string) => `${layerId}.eraser_line_${lineId}`; +export const getRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; +export const getObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts index fd95d2409a..d69c14afa3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers.ts @@ -10,11 +10,13 @@ import { getCALayerImageId, getIILayerImageId, getLayerBboxId, - getRGLayerObjectGroupId, + getObjectGroupId, INITIAL_IMAGE_LAYER_IMAGE_NAME, INITIAL_IMAGE_LAYER_NAME, LAYER_BBOX_NAME, NO_LAYERS_MESSAGE_LAYER_ID, + RASTER_LAYER_NAME, + RASTER_LAYER_OBJECT_GROUP_NAME, RG_LAYER_LINE_NAME, RG_LAYER_NAME, RG_LAYER_OBJECT_GROUP_NAME, @@ -30,6 +32,7 @@ import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/control import { isControlAdapterLayer, isInitialImageLayer, + isRasterLayer, isRegionalGuidanceLayer, isRenderableLayer, } from 'features/controlLayers/store/controlLayersSlice'; @@ -39,15 +42,17 @@ import type { EraserLine, InitialImageLayer, Layer, + RasterLayer, RectShape, RegionalGuidanceLayer, + RgbaColor, Tool, } from 'features/controlLayers/store/types'; +import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; -import type { RgbColor } from 'react-colorful'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -59,21 +64,6 @@ import { TRANSPARENCY_CHECKER_PATTERN, } from './constants'; -const mapId = (object: { id: string }): string => object.id; - -/** - * Konva selection callback to select all renderable layers. This includes RG, CA and II layers. - */ -const selectRenderableLayers = (n: Konva.Node): boolean => - n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME || n.name() === INITIAL_IMAGE_LAYER_NAME; - -/** - * Konva selection callback to select RG mask objects. This includes lines and rects. - */ -const selectVectorMaskObjects = (node: Konva.Node): boolean => { - return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; -}; - /** * Creates the singleton tool preview layer and all its objects. * @param stage The konva stage @@ -130,7 +120,7 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => { const renderToolPreview = ( stage: Konva.Stage, tool: Tool, - color: RgbColor | null, + brushColor: RgbaColor, selectedLayerType: Layer['type'] | null, globalMaskLayerOpacity: number, cursorPos: Vector2d | null, @@ -142,7 +132,7 @@ const renderToolPreview = ( if (layerCount === 0) { // We have no layers, so we should not render any tool stage.container().style.cursor = 'default'; - } else if (selectedLayerType !== 'regional_guidance_layer') { + } else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { // Non-mask-guidance layers don't have tools stage.container().style.cursor = 'not-allowed'; } else if (tool === 'move') { @@ -173,14 +163,14 @@ const renderToolPreview = ( assert(rectPreview, 'Rect preview not found'); // No need to render the brush preview if the cursor position or color is missing - if (cursorPos && color && (tool === 'brush' || tool === 'eraser')) { + if (cursorPos && (tool === 'brush' || tool === 'eraser')) { // Update the fill circle const brushPreviewFill = brushPreviewGroup.findOne(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`); brushPreviewFill?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2, - fill: rgbaColorToString({ ...color, a: globalMaskLayerOpacity }), + fill: rgbaColorToString(brushColor), globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', }); @@ -263,7 +253,7 @@ const createRGLayer = ( // The object group holds all of the layer's objects (e.g. lines and rects) const konvaObjectGroup = new Konva.Group({ - id: getRGLayerObjectGroupId(layerState.id, uuidv4()), + id: getObjectGroupId(layerState.id, uuidv4()), name: RG_LAYER_OBJECT_GROUP_NAME, listening: false, }); @@ -273,13 +263,14 @@ const createRGLayer = ( return konvaLayer; }; +//#endregion /** - * Creates a konva vector mask brush line from a vector mask line. - * @param brushLine The vector mask line state + * Creates a konva line for a brush line. + * @param brushLine The brush line state * @param layerObjectGroup The konva layer's object group to add the line to */ -const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { +const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => { const konvaLine = new Konva.Line({ id: brushLine.id, key: brushLine.id, @@ -291,17 +282,18 @@ const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva shadowForStrokeEnabled: false, globalCompositeOperation: 'source-over', listening: false, + stroke: rgbaColorToString(brushLine.color), }); layerObjectGroup.add(konvaLine); return konvaLine; }; /** - * Creates a konva vector mask eraser line from a vector mask line. - * @param eraserLine The vector mask line state + * Creates a konva line for a eraser line. + * @param eraserLine The eraser line state * @param layerObjectGroup The konva layer's object group to add the line to */ -const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { +const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { const konvaLine = new Konva.Line({ id: eraserLine.id, key: eraserLine.id, @@ -313,42 +305,35 @@ const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Ko shadowForStrokeEnabled: false, globalCompositeOperation: 'destination-out', listening: false, + stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), }); layerObjectGroup.add(konvaLine); return konvaLine; }; -const createVectorMaskLine = (maskObject: BrushLine | EraserLine, layerObjectGroup: Konva.Group): Konva.Line => { - if (maskObject.type === 'brush_line') { - return createVectorMaskBrushLine(maskObject, layerObjectGroup); - } else { - // maskObject.type === 'eraser_line' - return createVectorMaskEraserLine(maskObject, layerObjectGroup); - } -}; - /** - * Creates a konva rect from a vector mask rect. - * @param vectorMaskRect The vector mask rect state + * Creates a konva rect for a rect shape. + * @param rectShape The rect shape state * @param layerObjectGroup The konva layer's object group to add the line to */ -const createVectorMaskRect = (vectorMaskRect: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { +const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => { const konvaRect = new Konva.Rect({ - id: vectorMaskRect.id, - key: vectorMaskRect.id, + id: rectShape.id, + key: rectShape.id, name: RG_LAYER_RECT_NAME, - x: vectorMaskRect.x, - y: vectorMaskRect.y, - width: vectorMaskRect.width, - height: vectorMaskRect.height, + x: rectShape.x, + y: rectShape.y, + width: rectShape.width, + height: rectShape.height, listening: false, + fill: rgbaColorToString(rectShape.color), }); layerObjectGroup.add(konvaRect); return konvaRect; }; /** - * Creates the "compositing rect" for a layer. + * Creates the "compositing rect" for a regional guidance layer. * @param konvaLayer The konva layer */ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { @@ -358,7 +343,7 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { }; /** - * Renders a regional guidance layer. + * Renders a raster layer. * @param stage The konva stage * @param layerState The regional guidance layer state * @param globalMaskLayerOpacity The global mask layer opacity @@ -391,7 +376,7 @@ const renderRGLayer = ( // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. let groupNeedsCache = false; - const objectIds = layerState.maskObjects.map(mapId); + const objectIds = layerState.objects.map(mapId); // Destroy any objects that are no longer in the redux state for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { if (!objectIds.includes(objectNode.id())) { @@ -400,29 +385,41 @@ const renderRGLayer = ( } } - for (const maskObject of layerState.maskObjects) { - if (maskObject.type === 'brush_line' || maskObject.type === 'eraser_line') { - const vectorMaskLine = - stage.findOne(`#${maskObject.id}`) ?? createVectorMaskLine(maskObject, konvaObjectGroup); + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); // Only update the points if they have changed. The point values are never mutated, they are only added to the // array, so checking the length is sufficient to determine if we need to re-cache. - if (vectorMaskLine.points().length !== maskObject.points.length) { - vectorMaskLine.points(maskObject.points); + if (konvaBrushLine.points().length !== obj.points.length) { + konvaBrushLine.points(obj.points); groupNeedsCache = true; } // Only update the color if it has changed. - if (vectorMaskLine.stroke() !== rgbColor) { - vectorMaskLine.stroke(rgbColor); + if (konvaBrushLine.stroke() !== rgbColor) { + konvaBrushLine.stroke(rgbColor); groupNeedsCache = true; } - } else if (maskObject.type === 'rect_shape') { - const konvaObject = - stage.findOne(`#${maskObject.id}`) ?? createVectorMaskRect(maskObject, konvaObjectGroup); + } else if (obj.type === 'eraser_line') { + const konvaEraserLine = stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup); + + // Only update the points if they have changed. The point values are never mutated, they are only added to the + // array, so checking the length is sufficient to determine if we need to re-cache. + if (konvaEraserLine.points().length !== obj.points.length) { + konvaEraserLine.points(obj.points); + groupNeedsCache = true; + } + // Only update the color if it has changed. + if (konvaEraserLine.stroke() !== rgbColor) { + konvaEraserLine.stroke(rgbColor); + groupNeedsCache = true; + } + } else if (obj.type === 'rect_shape') { + const konvaRectShape = stage.findOne(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup); // Only update the color if it has changed. - if (konvaObject.fill() !== rgbColor) { - konvaObject.fill(rgbColor); + if (konvaRectShape.fill() !== rgbColor) { + konvaRectShape.fill(rgbColor); groupNeedsCache = true; } } @@ -485,6 +482,126 @@ const renderRGLayer = ( } }; +/** + * Creates a raster layer. + * @param stage The konva stage + * @param layerState The raster layer state + * @param onLayerPosChanged Callback for when the layer's position changes + */ +const createRasterLayer = ( + stage: Konva.Stage, + layerState: RasterLayer, + onLayerPosChanged?: (layerId: string, x: number, y: number) => void +): Konva.Layer => { + // This layer hasn't been added to the konva state yet + const konvaLayer = new Konva.Layer({ + id: layerState.id, + name: RASTER_LAYER_NAME, + draggable: true, + dragDistance: 0, + }); + + // When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing + // the position - we do not need to call this on the `dragmove` event. + if (onLayerPosChanged) { + konvaLayer.on('dragend', function (e) { + onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); + }); + } + + // The dragBoundFunc limits how far the layer can be dragged + konvaLayer.dragBoundFunc(function (pos) { + const cursorPos = getScaledFlooredCursorPosition(stage); + if (!cursorPos) { + return this.getAbsolutePosition(); + } + // Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds + if ( + cursorPos.x < 0 || + cursorPos.x > stage.width() / stage.scaleX() || + cursorPos.y < 0 || + cursorPos.y > stage.height() / stage.scaleY() + ) { + return this.getAbsolutePosition(); + } + return pos; + }); + + // The object group holds all of the layer's objects (e.g. lines and rects) + const konvaObjectGroup = new Konva.Group({ + id: getObjectGroupId(layerState.id, uuidv4()), + name: RASTER_LAYER_OBJECT_GROUP_NAME, + listening: false, + }); + konvaLayer.add(konvaObjectGroup); + + stage.add(konvaLayer); + + return konvaLayer; +}; + +/** + * Renders a regional guidance layer. + * @param stage The konva stage + * @param layerState The regional guidance layer state + * @param tool The current tool + * @param onLayerPosChanged Callback for when the layer's position changes + */ +const renderRasterLayer = ( + stage: Konva.Stage, + layerState: RasterLayer, + tool: Tool, + onLayerPosChanged?: (layerId: string, x: number, y: number) => void +): void => { + const konvaLayer = + stage.findOne(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onLayerPosChanged); + + // Update the layer's position and listening state + konvaLayer.setAttrs({ + listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events + x: Math.floor(layerState.x), + y: Math.floor(layerState.y), + }); + + const konvaObjectGroup = konvaLayer.findOne(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`); + assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`); + + const objectIds = layerState.objects.map(mapId); + // Destroy any objects that are no longer in the redux state + for (const objectNode of konvaObjectGroup.getChildren()) { + if (!objectIds.includes(objectNode.id())) { + objectNode.destroy(); + } + } + + for (const obj of layerState.objects) { + if (obj.type === 'brush_line') { + const konvaBrushLine = stage.findOne(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup); + // Only update the points if they have changed. + if (konvaBrushLine.points().length !== obj.points.length) { + konvaBrushLine.points(obj.points); + } + } else if (obj.type === 'eraser_line') { + const konvaEraserLine = stage.findOne(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup); + // Only update the points if they have changed. + if (konvaEraserLine.points().length !== obj.points.length) { + konvaEraserLine.points(obj.points); + } + } else if (obj.type === 'rect_shape') { + if (!stage.findOne(`#${obj.id}`)) { + createRectShape(obj, konvaObjectGroup); + } + } + } + + // Only update layer visibility if it has changed. + if (konvaLayer.visible() !== layerState.isEnabled) { + konvaLayer.visible(layerState.isEnabled); + } + + konvaObjectGroup.opacity(layerState.opacity); +}; + /** * Creates an initial image konva layer. * @param stage The konva stage @@ -805,6 +922,9 @@ const renderLayers = ( if (isInitialImageLayer(layer)) { renderIILayer(stage, layer, getImageDTO); } + if (isRasterLayer(layer)) { + renderRasterLayer(stage, layer, tool, onLayerPosChanged); + } // IP Adapter layers are not rendered } }; @@ -886,7 +1006,7 @@ const updateBboxes = ( const visible = bboxRect.visible(); bboxRect.visible(false); - if (rgLayer.maskObjects.length === 0) { + if (rgLayer.objects.length === 0) { // No objects - no bbox to calculate onBboxChanged(rgLayer.id, null); } else { @@ -1041,3 +1161,23 @@ export const debouncedRenderers = { arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS), updateBboxes: debounce(updateBboxes, DEBOUNCE_MS), }; + +//#region util +const mapId = (object: { id: string }): string => object.id; + +/** + * Konva selection callback to select all renderable layers. This includes RG, CA and II layers. + */ +const selectRenderableLayers = (n: Konva.Node): boolean => + n.name() === RG_LAYER_NAME || + n.name() === CA_LAYER_NAME || + n.name() === INITIAL_IMAGE_LAYER_NAME || + n.name() === RASTER_LAYER_NAME; + +/** + * Konva selection callback to select RG mask objects. This includes lines and rects. + */ +const selectVectorMaskObjects = (node: Konva.Node): boolean => { + return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; +}; +//#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index b0cf1707f0..16069daecb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -5,12 +5,13 @@ import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/ import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { + getBrushLineId, getCALayerId, + getEraserLineId, getIPALayerId, getRasterLayerId, + getRectId, getRGLayerId, - getRGLayerLineId, - getRGLayerRectId, INITIAL_IMAGE_LAYER_ID, } from 'features/controlLayers/konva/naming'; import type { @@ -45,20 +46,24 @@ import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { - AddLineArg, + AddBrushLineArg, + AddEraserLineArg, AddPointToLineArg, - AddRectArg, + AddRectShapeArg, BrushLine, ControlAdapterLayer, ControlLayersState, - DrawingTool, + EllipseShape, EraserLine, + ImageObject, InitialImageLayer, IPAdapterLayer, Layer, + PolygonShape, RasterLayer, RectShape, RegionalGuidanceLayer, + RgbaColor, Tool, } from './types'; import { DEFAULT_RGBA_COLOR } from './types'; @@ -67,6 +72,7 @@ export const initialControlLayersState: ControlLayersState = { _version: 3, selectedLayerId: null, brushSize: 100, + brushColor: DEFAULT_RGBA_COLOR, layers: [], globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity positivePrompt: '', @@ -81,8 +87,9 @@ export const initialControlLayersState: ControlLayersState = { }, }; -const isLine = (obj: BrushLine | EraserLine | RectShape): obj is BrushLine | EraserLine => - obj.type === 'brush_line' || obj.type === 'eraser_line'; +const isLine = ( + obj: BrushLine | EraserLine | RectShape | EllipseShape | PolygonShape | ImageObject +): obj is BrushLine => obj.type === 'brush_line' || obj.type === 'eraser_line'; export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => layer?.type === 'regional_guidance_layer'; export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => @@ -131,6 +138,14 @@ const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): Regio assert(isRegionalGuidanceLayer(layer)); return layer; }; +const selectRGOrRasterLayerOrThrow = ( + state: ControlLayersState, + layerId: string +): RegionalGuidanceLayer | RasterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isRegionalGuidanceLayer(layer) || isRasterLayer(layer)); + return layer; +}; export const selectRGLayerIPAdapterOrThrow = ( state: ControlLayersState, layerId: string, @@ -187,7 +202,7 @@ export const controlLayersSlice = createSlice({ layer.bboxNeedsUpdate = false; if (bbox === null && layer.type === 'regional_guidance_layer') { // The layer was fully erased, empty its objects to prevent accumulation of invisible objects - layer.maskObjects = []; + layer.objects = []; layer.uploadedMaskImage = null; } } @@ -196,7 +211,7 @@ export const controlLayersSlice = createSlice({ const layer = state.layers.find((l) => l.id === action.payload); // TODO(psyche): Should other layer types also have reset functionality? if (isRegionalGuidanceLayer(layer)) { - layer.maskObjects = []; + layer.objects = []; layer.bbox = null; layer.isEnabled = true; layer.bboxNeedsUpdate = false; @@ -455,7 +470,7 @@ export const controlLayersSlice = createSlice({ isEnabled: true, bbox: null, bboxNeedsUpdate: false, - maskObjects: [], + objects: [], previewColor: getVectorMaskPreviewColor(state), x: 0, y: 0, @@ -490,81 +505,102 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayerOrThrow(state, layerId); layer.previewColor = color; }, - rgLayerLineAdded: { + brushLineAdded: { reducer: ( state, - action: PayloadAction<{ - layerId: string; - points: [number, number, number, number]; - tool: DrawingTool; - lineUuid: string; - }> + action: PayloadAction< + AddBrushLineArg & { + lineUuid: string; + } + > ) => { - const { layerId, points, tool, lineUuid } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); - const lineId = getRGLayerLineId(layer.id, lineUuid); - if (tool === 'brush') { - layer.maskObjects.push({ - id: lineId, - type: 'brush_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - color: DEFAULT_RGBA_COLOR, - }); - } else { - layer.maskObjects.push({ - id: lineId, - type: 'eraser_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - }); - } + const { layerId, points, lineUuid, color } = action.payload; + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + layer.objects.push({ + id: getBrushLineId(layer.id, lineUuid), + type: 'brush_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + color, + }); layer.bboxNeedsUpdate = true; - layer.uploadedMaskImage = null; + if (layer.type === 'regional_guidance_layer') { + layer.uploadedMaskImage = null; + } }, - prepare: (payload: AddLineArg) => ({ + prepare: (payload: AddBrushLineArg) => ({ payload: { ...payload, lineUuid: uuidv4() }, }), }, - rgLayerPointsAdded: (state, action: PayloadAction) => { + eraserLineAdded: { + reducer: ( + state, + action: PayloadAction< + AddEraserLineArg & { + lineUuid: string; + } + > + ) => { + const { layerId, points, lineUuid } = action.payload; + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + layer.objects.push({ + id: getEraserLineId(layer.id, lineUuid), + type: 'eraser_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + }); + layer.bboxNeedsUpdate = true; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } + }, + prepare: (payload: AddEraserLineArg) => ({ + payload: { ...payload, lineUuid: uuidv4() }, + }), + }, + linePointsAdded: (state, action: PayloadAction) => { const { layerId, point } = action.payload; - const layer = selectRGLayerOrThrow(state, layerId); - const lastLine = layer.maskObjects.findLast(isLine); - if (!lastLine) { + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + const lastLine = layer.objects.findLast(isLine); + if (!lastLine || !isLine(lastLine)) { return; } // Points must be offset by the layer's x and y coordinates // TODO: Handle this in the event listener lastLine.points.push(point[0] - layer.x, point[1] - layer.y); layer.bboxNeedsUpdate = true; - layer.uploadedMaskImage = null; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } }, - rgLayerRectAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => { - const { layerId, rect, rectUuid } = action.payload; + rectAdded: { + reducer: (state, action: PayloadAction) => { + const { layerId, rect, rectUuid, color } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles return; } - const layer = selectRGLayerOrThrow(state, layerId); - const id = getRGLayerRectId(layer.id, rectUuid); - layer.maskObjects.push({ + const layer = selectRGOrRasterLayerOrThrow(state, layerId); + const id = getRectId(layer.id, rectUuid); + layer.objects.push({ type: 'rect_shape', id, x: rect.x - layer.x, y: rect.y - layer.y, width: rect.width, height: rect.height, - color: DEFAULT_RGBA_COLOR, + color, }); layer.bboxNeedsUpdate = true; - layer.uploadedMaskImage = null; + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } }, - prepare: (payload: AddRectArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), + prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), }, rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => { const { layerId, imageDTO } = action.payload; @@ -776,6 +812,9 @@ export const controlLayersSlice = createSlice({ brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = Math.round(action.payload); }, + brushColorChanged: (state, action: PayloadAction) => { + state.brushColor = action.payload; + }, globalMaskLayerOpacityChanged: (state, action: PayloadAction) => { state.globalMaskLayerOpacity = action.payload; }, @@ -892,9 +931,10 @@ export const { rgLayerPositivePromptChanged, rgLayerNegativePromptChanged, rgLayerPreviewColorChanged, - rgLayerLineAdded, - rgLayerPointsAdded, - rgLayerRectAdded, + brushLineAdded, + eraserLineAdded, + linePointsAdded, + rectAdded, rgLayerMaskImageUploaded, rgLayerAutoNegativeChanged, rgLayerIPAdapterAdded, @@ -924,6 +964,7 @@ export const { heightChanged, aspectRatioChanged, brushSizeChanged, + brushColorChanged, globalMaskLayerOpacityChanged, undo, redo, @@ -960,9 +1001,9 @@ export const $lastAddedPoint = atom(null); // Some nanostores that are manually synced to redux state to provide imperative access // TODO(psyche): This is a hack, figure out another way to handle this... export const $brushSize = atom(0); +export const $brushColor = atom(DEFAULT_RGBA_COLOR); export const $brushSpacingPx = atom(0); -export const $selectedLayerId = atom(null); -export const $selectedLayerType = atom(null); +export const $selectedLayer = atom(null); export const $shouldInvertBrushSizeScrollDirection = atom(false); export const controlLayersPersistConfig: PersistConfig = { @@ -998,10 +1039,10 @@ export const controlLayersUndoableConfig: UndoableOptions { - // Ignore all actions from other slices - if (!action.type.startsWith(controlLayersSlice.name)) { - return false; - } - // This action is triggered on state changes, including when we undo. If we do not ignore this action, when we - // undo, this action triggers and empties the future states array. Therefore, we must ignore this action. - if (layerBboxChanged.match(action)) { - return false; - } - return true; + return false; }, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 03c47da357..ab40c25824 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -55,7 +55,7 @@ const zRgbColor = z.object({ const zRgbaColor = zRgbColor.extend({ a: z.number().min(0).max(1), }); -type RgbaColor = z.infer; +export type RgbaColor = z.infer; export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); @@ -193,7 +193,7 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); -const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ +const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({ type: z.literal('regional_guidance_layer'), maskObjects: z.array(zMaskObject), positivePrompt: zParameterPositivePrompt.nullable(), @@ -203,7 +203,28 @@ const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ autoNegative: zAutoNegative, uploadedMaskImage: zImageWithDims.nullable(), }); -export type RegionalGuidanceLayer = z.infer; +const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ + type: z.literal('regional_guidance_layer'), + objects: z.array(zMaskObject), + positivePrompt: zParameterPositivePrompt.nullable(), + negativePrompt: zParameterNegativePrompt.nullable(), + ipAdapters: z.array(zIPAdapterConfigV2), + previewColor: zRgbColor, + autoNegative: zAutoNegative, + uploadedMaskImage: zImageWithDims.nullable(), +}); +const zRGLayer = z + .union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer]) + .transform((val) => { + if ('maskObjects' in val) { + const { maskObjects, ...rest } = val; + return { ...rest, objects: maskObjects }; + } else { + return val; + } + }) + .pipe(zRegionalGuidanceLayer); +export type RegionalGuidanceLayer = z.infer; const zInitialImageLayer = zRenderableLayerBase.extend({ type: z.literal('initial_image_layer'), @@ -227,6 +248,7 @@ export type ControlLayersState = { selectedLayerId: string | null; layers: Layer[]; brushSize: number; + brushColor: RgbaColor; globalMaskLayerOpacity: number; positivePrompt: ParameterPositivePrompt; negativePrompt: ParameterNegativePrompt; @@ -240,6 +262,7 @@ export type ControlLayersState = { }; }; -export type AddLineArg = { layerId: string; points: [number, number, number, number]; tool: DrawingTool }; +export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; +export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; export type AddPointToLineArg = { layerId: string; point: [number, number] }; -export type AddRectArg = { layerId: string; rect: IRect }; +export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor };