mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): mask fill patterns
This commit is contained in:
parent
d4b0dbce49
commit
43b3fab6be
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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';
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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,
|
||||
|
@ -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>;
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user