feat(ui): mask fill patterns

This commit is contained in:
psychedelicious 2024-08-16 18:56:26 +10:00
parent d4b0dbce49
commit 43b3fab6be
16 changed files with 283 additions and 61 deletions

View File

@ -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",

View File

@ -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(() => {
<Popover isLazy>
<PopoverTrigger>
<Flex
as="button"
role="button"
aria-label={t('controlLayers.maskPreviewColor')}
borderRadius="full"
borderWidth={1}
bg={rgbColorToString(fill)}
bg={rgbColorToString(fill.color)}
w={8}
h={8}
cursor="pointer"
tabIndex={-1}
onDoubleClick={stopPropagation} // double click expands the layer
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<RgbColorPicker color={fill} onChange={onChange} withNumberInput />
<Flex flexDir="column" gap={4}>
<RgbColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<MaskFillStyle style={fill.style} onChange={onChangeFillStyle} />
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>

View File

@ -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(() => {
<Popover isLazy>
<PopoverTrigger>
<Flex
as="button"
role="button"
aria-label={t('controlLayers.maskPreviewColor')}
borderRadius="full"
borderWidth={1}
bg={rgbColorToString(fill)}
bg={rgbColorToString(fill.color)}
w={8}
h={8}
cursor="pointer"
tabIndex={-1}
onDoubleClick={stopPropagation} // double click expands the layer
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<RgbColorPicker color={fill} onChange={onChange} withNumberInput />
<Flex flexDir="column" gap={4}>
<RgbColorPicker color={fill.color} onChange={onChangeFillColor} withNumberInput />
<MaskFillStyle style={fill.style} onChange={onChangeFillStyle} />
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>

View File

@ -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<ComboboxOnChange>(
(v) => {
if (!isFillStyle(v?.value)) {
return;
}
onChange(v.value);
},
[onChange]
);
const options = useMemo<ComboboxOption[]>(() => {
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 (
<FormControl>
<FormLabel m={0}>{t('controlLayers.fill.fillStyle')}</FormLabel>
<Combobox value={value} options={options} onChange={_onChange} isSearchable={false} />
</FormControl>
);
});
MaskFillStyle.displayName = 'MaskFillStyle';

View File

@ -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<typeof getPatternSVG>): 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);
}
};
/**

View File

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

View File

@ -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<FillStyle, 'solid'>, 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;
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="6px" height="6px" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M0,10L9,1" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M3,13L14,2" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M-1,5L8,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M1,-2L13,10" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M-1,2L10,13" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M5,-4L14,5" style="fill:none;stroke:black;stroke-width:1px;"/>
</svg>

After

Width:  |  Height:  |  Size: 920 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="9px" height="9px" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M9.9,7.2L7.2,9.9" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M9.9,2.7L2.7,9.9" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M9,-0.9L-0.9,9" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M4.5,-0.9L-0.9,4.5" style="fill:none;stroke:black;stroke-width:1px;"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="12px" height="12px" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M0.5,-1L0.5,12" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M4.5,-1L4.5,12" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M8.5,-1L8.5,12" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M-1,0.5L12,0.5" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M-1,4.5L12,4.5" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M-1,8.5L12,8.5" style="fill:none;stroke:black;stroke-width:1px;"/>
</svg>

After

Width:  |  Height:  |  Size: 945 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="9px" height="9px" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M10,0.5L-1,0.5" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M10,3.5L-1,3.5" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M10,6.5L-1,6.5" style="fill:none;stroke:black;stroke-width:1px;"/>
</svg>

After

Width:  |  Height:  |  Size: 703 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="9px" height="9px" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M0.5,-1L0.5,10" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M3.5,-1L3.5,10" style="fill:none;stroke:black;stroke-width:1px;"/>
<path d="M6.5,-1L6.5,10" style="fill:none;stroke:black;stroke-width:1px;"/>
</svg>

After

Width:  |  Height:  |  Size: 703 B

View File

@ -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,

View File

@ -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<CanvasV2State>;

View File

@ -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);

View File

@ -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<typeof zFillStyle>;
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<typeof zFill>;
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<typeof zCanvasRegionalGuidanceState>;
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),
});