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 };