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