From 43b3fab6be23d724208c8bd0cb80670350a20c48 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 16 Aug 2024 18:56:26 +1000
Subject: [PATCH] feat(ui): mask fill patterns
---
invokeai/frontend/web/public/locales/en.json | 9 +++
.../InpaintMaskMaskFillColorPicker.tsx | 26 +++++---
.../RegionalGuidanceMaskFillColorPicker.tsx | 26 +++++---
.../components/common/MaskFillStyle.tsx | 64 ++++++++++++++++++
.../konva/CanvasObjectRenderer.ts | 65 +++++++++++++------
.../controlLayers/konva/CanvasStateApi.ts | 2 +-
.../konva/patterns/getPatternSVG.ts | 27 ++++++++
.../konva/patterns/pattern-crosshatch.svg | 13 ++++
.../konva/patterns/pattern-diagonal.svg | 11 ++++
.../konva/patterns/pattern-grid.svg | 13 ++++
.../konva/patterns/pattern-horizontal.svg | 10 +++
.../konva/patterns/pattern-vertical.svg | 10 +++
.../controlLayers/store/canvasV2Slice.ts | 11 +++-
.../store/inpaintMaskReducers.ts | 15 +++--
.../controlLayers/store/regionsReducers.ts | 23 +++++--
.../src/features/controlLayers/store/types.ts | 19 +++---
16 files changed, 283 insertions(+), 61 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts
create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg
create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg
create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg
create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg
create mode 100644 invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index dc8c894549..756eed45b6 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1698,6 +1698,15 @@
"filter": "Filter",
"convertToControlLayer": "Convert to Control Layer",
"convertToRasterLayer": "Convert to Raster Layer",
+ "fill": {
+ "fillStyle": "Fill Style",
+ "solid": "Solid",
+ "grid": "Grid",
+ "crosshatch": "Crosshatch",
+ "vertical": "Vertical",
+ "horizontal": "Horizontal",
+ "diagonal": "Diagonal"
+ },
"tool": {
"brush": "Brush",
"eraser": "Eraser",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx
index 891b38a418..f7d834bfbc 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx
@@ -3,7 +3,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { stopPropagation } from 'common/util/stopPropagation';
-import { imFillChanged } from 'features/controlLayers/store/canvasV2Slice';
+import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
+import { imFillColorChanged, imFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
+import type { FillStyle } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
@@ -12,9 +14,15 @@ export const InpaintMaskMaskFillColorPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill);
- const onChange = useCallback(
- (fill: RgbColor) => {
- dispatch(imFillChanged({ fill }));
+ const onChangeFillColor = useCallback(
+ (color: RgbColor) => {
+ dispatch(imFillColorChanged({ color }));
+ },
+ [dispatch]
+ );
+ const onChangeFillStyle = useCallback(
+ (style: FillStyle) => {
+ dispatch(imFillStyleChanged({ style }));
},
[dispatch]
);
@@ -22,21 +30,23 @@ export const InpaintMaskMaskFillColorPicker = memo(() => {
-
+
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx
index 66e19c9b35..85bd4758f6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx
@@ -3,9 +3,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { stopPropagation } from 'common/util/stopPropagation';
+import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
-import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice';
+import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import type { FillStyle } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
@@ -15,9 +17,15 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
- const onChange = useCallback(
- (fill: RgbColor) => {
- dispatch(rgFillChanged({ id: entityIdentifier.id, fill }));
+ const onChangeFillColor = useCallback(
+ (color: RgbColor) => {
+ dispatch(rgFillColorChanged({ id: entityIdentifier.id, color }));
+ },
+ [dispatch, entityIdentifier.id]
+ );
+ const onChangeFillStyle = useCallback(
+ (style: FillStyle) => {
+ dispatch(rgFillStyleChanged({ id: entityIdentifier.id, style }));
},
[dispatch, entityIdentifier.id]
);
@@ -25,21 +33,23 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => {
-
+
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx
new file mode 100644
index 0000000000..e7c53d9f76
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/MaskFillStyle.tsx
@@ -0,0 +1,64 @@
+import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
+import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
+import type { FillStyle } from 'features/controlLayers/store/types';
+import { isFillStyle } from 'features/controlLayers/store/types';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type Props = {
+ style: FillStyle;
+ onChange: (style: FillStyle) => void;
+};
+
+export const MaskFillStyle = memo(({ style, onChange }: Props) => {
+ const { t } = useTranslation();
+ const _onChange = useCallback(
+ (v) => {
+ if (!isFillStyle(v?.value)) {
+ return;
+ }
+ onChange(v.value);
+ },
+ [onChange]
+ );
+
+ const options = useMemo(() => {
+ return [
+ {
+ value: 'solid',
+ label: t('controlLayers.fill.solid'),
+ },
+ {
+ value: 'diagonal',
+ label: t('controlLayers.fill.diagonal'),
+ },
+ {
+ value: 'crosshatch',
+ label: t('controlLayers.fill.crosshatch'),
+ },
+ {
+ value: 'grid',
+ label: t('controlLayers.fill.grid'),
+ },
+ {
+ value: 'horizontal',
+ label: t('controlLayers.fill.horizontal'),
+ },
+ {
+ value: 'vertical',
+ label: t('controlLayers.fill.vertical'),
+ },
+ ];
+ }, [t]);
+
+ const value = useMemo(() => options.find((o) => o.value === style), [options, style]);
+
+ return (
+
+ {t('controlLayers.fill.fillStyle')}
+
+
+ );
+});
+
+MaskFillStyle.displayName = 'MaskFillStyle';
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts
index e73a83aef7..d0a409ab94 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts
@@ -8,24 +8,35 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
+import { getPatternSVG } from 'features/controlLayers/konva/patterns/getPatternSVG';
import { getPrefixedId, konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasImageState,
CanvasRectState,
+ Fill,
ImageCache,
Rect,
- RgbColor,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import Konva from 'konva';
+import type { RectConfig } from 'konva/lib/shapes/Rect';
import { isEqual } from 'lodash-es';
import type { Logger } from 'roarr';
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
+function setFillPatternImage(shape: Konva.Shape, ...args: Parameters): HTMLImageElement {
+ const imageElement = new Image();
+ imageElement.onload = () => {
+ shape.fillPatternImage(imageElement);
+ };
+ imageElement.src = getPatternSVG(...args);
+ return imageElement;
+}
+
/**
* Union of all object renderers.
*/
@@ -86,7 +97,11 @@ export class CanvasObjectRenderer {
*
* The compositing rect is not added to the object group.
*/
- compositingRect: Konva.Rect | null;
+ compositing: {
+ group: Konva.Group;
+ rect: Konva.Rect;
+ patternImage: HTMLImageElement;
+ } | null;
};
constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) {
@@ -99,20 +114,26 @@ export class CanvasObjectRenderer {
this.konva = {
objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }),
- compositingRect: null,
+ compositing: null,
};
this.parent.konva.layer.add(this.konva.objectGroup);
if (this.parent.state.type === 'inpaint_mask' || this.parent.state.type === 'regional_guidance') {
- this.konva.compositingRect = new Konva.Rect({
+ const rect = new Konva.Rect({
name: `${this.type}:compositing_rect`,
globalCompositeOperation: 'source-in',
listening: false,
strokeEnabled: false,
perfectDrawEnabled: false,
});
- this.parent.konva.layer.add(this.konva.compositingRect);
+ this.konva.compositing = {
+ group: new Konva.Group({ name: `${this.type}:compositing_group`, listening: false }),
+ rect,
+ patternImage: new Image(), // we will set the src on this on the first render
+ };
+ this.konva.compositing.group.add(this.konva.compositing.rect);
+ this.parent.konva.layer.add(this.konva.compositing.group);
}
this.subscriptions.add(
@@ -126,14 +147,9 @@ export class CanvasObjectRenderer {
// The compositing rect must cover the whole stage at all times. When the stage is scaled, moved or resized, we
// need to update the compositing rect to match the stage.
this.subscriptions.add(
- this.manager.stateApi.$stageAttrs.listen(({ x, y, width, height, scale }) => {
- if (this.konva.compositingRect) {
- this.konva.compositingRect.setAttrs({
- x: -x / scale,
- y: -y / scale,
- width: width / scale,
- height: height / scale,
- });
+ this.manager.stateApi.$stageAttrs.listen(() => {
+ if (this.konva.compositing && this.parent.type === 'mask_adapter') {
+ this.updateCompositingRect(this.parent.state.fill, this.manager.stateApi.getMaskOpacity());
}
})
);
@@ -167,20 +183,31 @@ export class CanvasObjectRenderer {
return didRender;
};
- updateCompositingRect = (fill: RgbColor, opacity: number) => {
+ updateCompositingRect = (fill: Fill, opacity: number) => {
this.log.trace('Updating compositing rect');
- assert(this.konva.compositingRect, 'Missing compositing rect');
+ assert(this.konva.compositing, 'Missing compositing rect');
- const rgbColor = rgbColorToString(fill);
const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get();
- this.konva.compositingRect.setAttrs({
- fill: rgbColor,
+ console.log('stageAttrs', this.manager.stateApi.$stageAttrs.get());
+ const attrs: RectConfig = {
opacity,
x: -x / scale,
y: -y / scale,
width: width / scale,
height: height / scale,
- });
+ };
+
+ if (fill.style === 'solid') {
+ attrs.fill = rgbColorToString(fill.color);
+ attrs.fillPriority = 'color';
+ this.konva.compositing.rect.setAttrs(attrs);
+ } else {
+ attrs.fillPatternScaleX = 1 / scale;
+ attrs.fillPatternScaleY = 1 / scale;
+ attrs.fillPriority = 'pattern';
+ this.konva.compositing.rect.setAttrs(attrs);
+ setFillPatternImage(this.konva.compositing.rect, fill.style, fill.color);
+ }
};
/**
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts
index ed67ebda22..e96e6c3ea0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts
@@ -248,7 +248,7 @@ export class CanvasStateApi {
if (selectedEntity) {
// The brush should use the mask opacity for these entity types
if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') {
- currentFill = { ...selectedEntity.state.fill, a: this.getSettings().maskOpacity };
+ currentFill = { ...selectedEntity.state.fill.color, a: this.getSettings().maskOpacity };
}
}
return currentFill;
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts
new file mode 100644
index 0000000000..2836b13979
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/getPatternSVG.ts
@@ -0,0 +1,27 @@
+import { rgbColorToString } from 'common/util/colorCodeTransformers';
+import type { FillStyle, RgbColor } from 'features/controlLayers/store/types';
+
+import crosshatch from './pattern-crosshatch.svg?raw';
+import diagonal from './pattern-diagonal.svg?raw';
+import grid from './pattern-grid.svg?raw';
+import horizontal from './pattern-horizontal.svg?raw';
+import vertical from './pattern-vertical.svg?raw';
+
+export function getPatternSVG(pattern: Exclude, color: RgbColor) {
+ let content: string = 'data:image/svg+xml;utf8,';
+ if (pattern === 'crosshatch') {
+ content += crosshatch;
+ } else if (pattern === 'diagonal') {
+ content += diagonal;
+ } else if (pattern === 'horizontal') {
+ content += horizontal;
+ } else if (pattern === 'vertical') {
+ content += vertical;
+ } else if (pattern === 'grid') {
+ content += grid;
+ }
+
+ content = content.replaceAll('stroke:black', `stroke:${rgbColorToString(color)}`);
+
+ return content;
+}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg
new file mode 100644
index 0000000000..4bf6edaad9
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-crosshatch.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg
new file mode 100644
index 0000000000..642bb7bdee
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-diagonal.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg
new file mode 100644
index 0000000000..a5f0089617
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-grid.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg
new file mode 100644
index 0000000000..d710b81895
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-horizontal.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg
new file mode 100644
index 0000000000..879a8c0445
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/patterns/pattern-vertical.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
index 8624e46741..3f9d53b6a5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
@@ -53,7 +53,10 @@ const initialState: CanvasV2State = {
inpaintMask: {
id: 'inpaint_mask',
type: 'inpaint_mask',
- fill: RGBA_RED,
+ fill: {
+ style: 'diagonal',
+ color: RGBA_RED,
+ },
rasterizationCache: [],
isEnabled: true,
objects: [],
@@ -531,7 +534,8 @@ export const {
rgAllDeleted,
rgPositivePromptChanged,
rgNegativePromptChanged,
- rgFillChanged,
+ rgFillColorChanged,
+ rgFillStyleChanged,
rgAutoNegativeChanged,
rgIPAdapterAdded,
rgIPAdapterDeleted,
@@ -587,7 +591,8 @@ export const {
loraAllDeleted,
// Inpaint mask
imRecalled,
- imFillChanged,
+ imFillColorChanged,
+ imFillStyleChanged,
// Staging
sessionStartedStaging,
sessionImageStaged,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts
index 2950dc60b7..b1a954ca5c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts
@@ -1,7 +1,6 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
-import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types';
-
-import type { RgbColor } from './types';
+import type { CanvasInpaintMaskState, CanvasV2State, FillStyle } from 'features/controlLayers/store/types';
+import type { RgbColor } from 'react-colorful';
export const inpaintMaskReducers = {
imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
@@ -9,8 +8,12 @@ export const inpaintMaskReducers = {
state.inpaintMask = data;
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
},
- imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => {
- const { fill } = action.payload;
- state.inpaintMask.fill = fill;
+ imFillColorChanged: (state, action: PayloadAction<{ color: RgbColor }>) => {
+ const { color } = action.payload;
+ state.inpaintMask.fill.color = color;
+ },
+ imFillStyleChanged: (state, action: PayloadAction<{ style: FillStyle }>) => {
+ const { style } = action.payload;
+ state.inpaintMask.fill.style = style;
},
} satisfies SliceCaseReducers;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts
index 4aca555381..6aa4a171c4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts
@@ -3,6 +3,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import type {
CanvasV2State,
CLIPVisionModelV2,
+ FillStyle,
IPMethodV2,
RegionalGuidanceIPAdapterConfig,
} from 'features/controlLayers/store/types';
@@ -42,7 +43,7 @@ const DEFAULT_MASK_COLORS: RgbColor[] = [
];
const getRGMaskFill = (state: CanvasV2State): RgbColor => {
- const lastFill = state.regions.entities.slice(-1)[0]?.fill;
+ const lastFill = state.regions.entities.slice(-1)[0]?.fill.color;
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
if (i === -1) {
i = 0;
@@ -63,7 +64,10 @@ export const regionsReducers = {
type: 'regional_guidance',
isEnabled: true,
objects: [],
- fill: getRGMaskFill(state),
+ fill: {
+ style: 'solid',
+ color: getRGMaskFill(state),
+ },
position: { x: 0, y: 0 },
autoNegative: 'invert',
positivePrompt: '',
@@ -100,14 +104,23 @@ export const regionsReducers = {
}
entity.negativePrompt = prompt;
},
- rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => {
- const { id, fill } = action.payload;
+ rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => {
+ const { id, color } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
if (!entity) {
return;
}
- entity.fill = fill;
+ entity.fill.color = color;
},
+ rgFillStyleChanged: (state, action: PayloadAction<{ id: string; style: FillStyle }>) => {
+ const { id, style } = action.payload;
+ const entity = selectRegionalGuidanceEntity(state, id);
+ if (!entity) {
+ return;
+ }
+ entity.fill.style = style;
+ },
+
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
const { id, autoNegative } = action.payload;
const rg = selectRegionalGuidanceEntity(state, id);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index a6d9115386..3598d4a412 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -641,6 +641,12 @@ const zMaskObject = z
})
.pipe(z.discriminatedUnion('type', [zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]));
+const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
+export type FillStyle = z.infer;
+export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
+const zFill = z.object({ style: zFillStyle, color: zRgbColor });
+export type Fill = z.infer;
+
const zImageCache = z.object({
imageName: z.string(),
rect: zRect,
@@ -665,7 +671,7 @@ export const zCanvasRegionalGuidanceState = z.object({
isEnabled: z.boolean(),
position: zCoordinate,
objects: z.array(zCanvasObjectState),
- fill: zRgbColor,
+ fill: zFill,
positivePrompt: zParameterPositivePrompt.nullable(),
negativePrompt: zParameterNegativePrompt.nullable(),
ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig),
@@ -674,21 +680,12 @@ export const zCanvasRegionalGuidanceState = z.object({
});
export type CanvasRegionalGuidanceState = z.infer;
-const zColorFill = z.object({
- type: z.literal('color_fill'),
- color: zRgbaColor,
-});
-const zImageFill = z.object({
- type: z.literal('image_fill'),
- src: z.string(),
-});
-const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]);
const zCanvasInpaintMaskState = z.object({
id: z.literal('inpaint_mask'),
type: z.literal('inpaint_mask'),
isEnabled: z.boolean(),
position: zCoordinate,
- fill: zRgbColor,
+ fill: zFill,
objects: z.array(zCanvasObjectState),
rasterizationCache: z.array(zImageCache),
});