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
c7ba7ac876
commit
6eae3470cd
@ -1703,6 +1703,15 @@
|
|||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"convertToControlLayer": "Convert to Control Layer",
|
"convertToControlLayer": "Convert to Control Layer",
|
||||||
"convertToRasterLayer": "Convert to Raster Layer",
|
"convertToRasterLayer": "Convert to Raster Layer",
|
||||||
|
"fill": {
|
||||||
|
"fillStyle": "Fill Style",
|
||||||
|
"solid": "Solid",
|
||||||
|
"grid": "Grid",
|
||||||
|
"crosshatch": "Crosshatch",
|
||||||
|
"vertical": "Vertical",
|
||||||
|
"horizontal": "Horizontal",
|
||||||
|
"diagonal": "Diagonal"
|
||||||
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"brush": "Brush",
|
"brush": "Brush",
|
||||||
"eraser": "Eraser",
|
"eraser": "Eraser",
|
||||||
|
@ -3,7 +3,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
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 { memo, useCallback } from 'react';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -12,9 +14,15 @@ export const InpaintMaskMaskFillColorPicker = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill);
|
const fill = useAppSelector((s) => s.canvasV2.inpaintMask.fill);
|
||||||
const onChange = useCallback(
|
const onChangeFillColor = useCallback(
|
||||||
(fill: RgbColor) => {
|
(color: RgbColor) => {
|
||||||
dispatch(imFillChanged({ fill }));
|
dispatch(imFillColorChanged({ color }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
const onChangeFillStyle = useCallback(
|
||||||
|
(style: FillStyle) => {
|
||||||
|
dispatch(imFillStyleChanged({ style }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -22,21 +30,23 @@ export const InpaintMaskMaskFillColorPicker = memo(() => {
|
|||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Flex
|
<Flex
|
||||||
as="button"
|
role="button"
|
||||||
aria-label={t('controlLayers.maskPreviewColor')}
|
aria-label={t('controlLayers.maskPreviewColor')}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
bg={rgbColorToString(fill)}
|
bg={rgbColorToString(fill.color)}
|
||||||
w={8}
|
w={8}
|
||||||
h={8}
|
h={8}
|
||||||
cursor="pointer"
|
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onDoubleClick={stopPropagation} // double click expands the layer
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverBody minH={64}>
|
<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>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -3,9 +3,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
|
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
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 { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
|
||||||
|
import type { FillStyle } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -15,9 +17,15 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
|
const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
|
||||||
const onChange = useCallback(
|
const onChangeFillColor = useCallback(
|
||||||
(fill: RgbColor) => {
|
(color: RgbColor) => {
|
||||||
dispatch(rgFillChanged({ id: entityIdentifier.id, fill }));
|
dispatch(rgFillColorChanged({ id: entityIdentifier.id, color }));
|
||||||
|
},
|
||||||
|
[dispatch, entityIdentifier.id]
|
||||||
|
);
|
||||||
|
const onChangeFillStyle = useCallback(
|
||||||
|
(style: FillStyle) => {
|
||||||
|
dispatch(rgFillStyleChanged({ id: entityIdentifier.id, style }));
|
||||||
},
|
},
|
||||||
[dispatch, entityIdentifier.id]
|
[dispatch, entityIdentifier.id]
|
||||||
);
|
);
|
||||||
@ -25,21 +33,23 @@ export const RegionalGuidanceMaskFillColorPicker = memo(() => {
|
|||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Flex
|
<Flex
|
||||||
as="button"
|
role="button"
|
||||||
aria-label={t('controlLayers.maskPreviewColor')}
|
aria-label={t('controlLayers.maskPreviewColor')}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
bg={rgbColorToString(fill)}
|
bg={rgbColorToString(fill.color)}
|
||||||
w={8}
|
w={8}
|
||||||
h={8}
|
h={8}
|
||||||
cursor="pointer"
|
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onDoubleClick={stopPropagation} // double click expands the layer
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverBody minH={64}>
|
<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>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</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 { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
|
import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
|
||||||
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
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 { getPrefixedId, konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import type {
|
||||||
CanvasBrushLineState,
|
CanvasBrushLineState,
|
||||||
CanvasEraserLineState,
|
CanvasEraserLineState,
|
||||||
CanvasImageState,
|
CanvasImageState,
|
||||||
CanvasRectState,
|
CanvasRectState,
|
||||||
|
Fill,
|
||||||
ImageCache,
|
ImageCache,
|
||||||
Rect,
|
Rect,
|
||||||
RgbColor,
|
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import type { RectConfig } from 'konva/lib/shapes/Rect';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
|
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
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.
|
* Union of all object renderers.
|
||||||
*/
|
*/
|
||||||
@ -86,7 +97,11 @@ export class CanvasObjectRenderer {
|
|||||||
*
|
*
|
||||||
* The compositing rect is not added to the object group.
|
* 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) {
|
constructor(parent: CanvasLayerAdapter | CanvasMaskAdapter) {
|
||||||
@ -99,20 +114,26 @@ export class CanvasObjectRenderer {
|
|||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }),
|
objectGroup: new Konva.Group({ name: `${this.type}:object_group`, listening: false }),
|
||||||
compositingRect: null,
|
compositing: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.parent.konva.layer.add(this.konva.objectGroup);
|
this.parent.konva.layer.add(this.konva.objectGroup);
|
||||||
|
|
||||||
if (this.parent.state.type === 'inpaint_mask' || this.parent.state.type === 'regional_guidance') {
|
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`,
|
name: `${this.type}:compositing_rect`,
|
||||||
globalCompositeOperation: 'source-in',
|
globalCompositeOperation: 'source-in',
|
||||||
listening: false,
|
listening: false,
|
||||||
strokeEnabled: false,
|
strokeEnabled: false,
|
||||||
perfectDrawEnabled: 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(
|
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
|
// 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.
|
// need to update the compositing rect to match the stage.
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.manager.stateApi.$stageAttrs.listen(({ x, y, width, height, scale }) => {
|
this.manager.stateApi.$stageAttrs.listen(() => {
|
||||||
if (this.konva.compositingRect) {
|
if (this.konva.compositing && this.parent.type === 'mask_adapter') {
|
||||||
this.konva.compositingRect.setAttrs({
|
this.updateCompositingRect(this.parent.state.fill, this.manager.stateApi.getMaskOpacity());
|
||||||
x: -x / scale,
|
|
||||||
y: -y / scale,
|
|
||||||
width: width / scale,
|
|
||||||
height: height / scale,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -167,20 +183,31 @@ export class CanvasObjectRenderer {
|
|||||||
return didRender;
|
return didRender;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCompositingRect = (fill: RgbColor, opacity: number) => {
|
updateCompositingRect = (fill: Fill, opacity: number) => {
|
||||||
this.log.trace('Updating compositing rect');
|
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();
|
const { x, y, width, height, scale } = this.manager.stateApi.$stageAttrs.get();
|
||||||
this.konva.compositingRect.setAttrs({
|
console.log('stageAttrs', this.manager.stateApi.$stageAttrs.get());
|
||||||
fill: rgbColor,
|
const attrs: RectConfig = {
|
||||||
opacity,
|
opacity,
|
||||||
x: -x / scale,
|
x: -x / scale,
|
||||||
y: -y / scale,
|
y: -y / scale,
|
||||||
width: width / scale,
|
width: width / scale,
|
||||||
height: height / 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) {
|
if (selectedEntity) {
|
||||||
// The brush should use the mask opacity for these entity types
|
// The brush should use the mask opacity for these entity types
|
||||||
if (selectedEntity.state.type === 'regional_guidance' || selectedEntity.state.type === 'inpaint_mask') {
|
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;
|
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: {
|
inpaintMask: {
|
||||||
id: 'inpaint_mask',
|
id: 'inpaint_mask',
|
||||||
type: 'inpaint_mask',
|
type: 'inpaint_mask',
|
||||||
fill: RGBA_RED,
|
fill: {
|
||||||
|
style: 'diagonal',
|
||||||
|
color: RGBA_RED,
|
||||||
|
},
|
||||||
rasterizationCache: [],
|
rasterizationCache: [],
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
objects: [],
|
objects: [],
|
||||||
@ -531,7 +534,8 @@ export const {
|
|||||||
rgAllDeleted,
|
rgAllDeleted,
|
||||||
rgPositivePromptChanged,
|
rgPositivePromptChanged,
|
||||||
rgNegativePromptChanged,
|
rgNegativePromptChanged,
|
||||||
rgFillChanged,
|
rgFillColorChanged,
|
||||||
|
rgFillStyleChanged,
|
||||||
rgAutoNegativeChanged,
|
rgAutoNegativeChanged,
|
||||||
rgIPAdapterAdded,
|
rgIPAdapterAdded,
|
||||||
rgIPAdapterDeleted,
|
rgIPAdapterDeleted,
|
||||||
@ -587,7 +591,8 @@ export const {
|
|||||||
loraAllDeleted,
|
loraAllDeleted,
|
||||||
// Inpaint mask
|
// Inpaint mask
|
||||||
imRecalled,
|
imRecalled,
|
||||||
imFillChanged,
|
imFillColorChanged,
|
||||||
|
imFillStyleChanged,
|
||||||
// Staging
|
// Staging
|
||||||
sessionStartedStaging,
|
sessionStartedStaging,
|
||||||
sessionImageStaged,
|
sessionImageStaged,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||||
import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types';
|
import type { CanvasInpaintMaskState, CanvasV2State, FillStyle } from 'features/controlLayers/store/types';
|
||||||
|
import type { RgbColor } from 'react-colorful';
|
||||||
import type { RgbColor } from './types';
|
|
||||||
|
|
||||||
export const inpaintMaskReducers = {
|
export const inpaintMaskReducers = {
|
||||||
imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
|
imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => {
|
||||||
@ -9,8 +8,12 @@ export const inpaintMaskReducers = {
|
|||||||
state.inpaintMask = data;
|
state.inpaintMask = data;
|
||||||
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
|
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
|
||||||
},
|
},
|
||||||
imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => {
|
imFillColorChanged: (state, action: PayloadAction<{ color: RgbColor }>) => {
|
||||||
const { fill } = action.payload;
|
const { color } = action.payload;
|
||||||
state.inpaintMask.fill = fill;
|
state.inpaintMask.fill.color = color;
|
||||||
|
},
|
||||||
|
imFillStyleChanged: (state, action: PayloadAction<{ style: FillStyle }>) => {
|
||||||
|
const { style } = action.payload;
|
||||||
|
state.inpaintMask.fill.style = style;
|
||||||
},
|
},
|
||||||
} satisfies SliceCaseReducers<CanvasV2State>;
|
} satisfies SliceCaseReducers<CanvasV2State>;
|
||||||
|
@ -3,6 +3,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
|||||||
import type {
|
import type {
|
||||||
CanvasV2State,
|
CanvasV2State,
|
||||||
CLIPVisionModelV2,
|
CLIPVisionModelV2,
|
||||||
|
FillStyle,
|
||||||
IPMethodV2,
|
IPMethodV2,
|
||||||
RegionalGuidanceIPAdapterConfig,
|
RegionalGuidanceIPAdapterConfig,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
@ -42,7 +43,7 @@ const DEFAULT_MASK_COLORS: RgbColor[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const getRGMaskFill = (state: CanvasV2State): 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));
|
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
|
||||||
if (i === -1) {
|
if (i === -1) {
|
||||||
i = 0;
|
i = 0;
|
||||||
@ -63,7 +64,10 @@ export const regionsReducers = {
|
|||||||
type: 'regional_guidance',
|
type: 'regional_guidance',
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
objects: [],
|
objects: [],
|
||||||
fill: getRGMaskFill(state),
|
fill: {
|
||||||
|
style: 'solid',
|
||||||
|
color: getRGMaskFill(state),
|
||||||
|
},
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
autoNegative: 'invert',
|
autoNegative: 'invert',
|
||||||
positivePrompt: '',
|
positivePrompt: '',
|
||||||
@ -100,14 +104,23 @@ export const regionsReducers = {
|
|||||||
}
|
}
|
||||||
entity.negativePrompt = prompt;
|
entity.negativePrompt = prompt;
|
||||||
},
|
},
|
||||||
rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => {
|
rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => {
|
||||||
const { id, fill } = action.payload;
|
const { id, color } = action.payload;
|
||||||
const entity = selectRegionalGuidanceEntity(state, id);
|
const entity = selectRegionalGuidanceEntity(state, id);
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return;
|
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 }>) => {
|
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
|
||||||
const { id, autoNegative } = action.payload;
|
const { id, autoNegative } = action.payload;
|
||||||
const rg = selectRegionalGuidanceEntity(state, id);
|
const rg = selectRegionalGuidanceEntity(state, id);
|
||||||
|
@ -641,6 +641,12 @@ const zMaskObject = z
|
|||||||
})
|
})
|
||||||
.pipe(z.discriminatedUnion('type', [zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]));
|
.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({
|
const zImageCache = z.object({
|
||||||
imageName: z.string(),
|
imageName: z.string(),
|
||||||
rect: zRect,
|
rect: zRect,
|
||||||
@ -665,7 +671,7 @@ export const zCanvasRegionalGuidanceState = z.object({
|
|||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
position: zCoordinate,
|
position: zCoordinate,
|
||||||
objects: z.array(zCanvasObjectState),
|
objects: z.array(zCanvasObjectState),
|
||||||
fill: zRgbColor,
|
fill: zFill,
|
||||||
positivePrompt: zParameterPositivePrompt.nullable(),
|
positivePrompt: zParameterPositivePrompt.nullable(),
|
||||||
negativePrompt: zParameterNegativePrompt.nullable(),
|
negativePrompt: zParameterNegativePrompt.nullable(),
|
||||||
ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig),
|
ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig),
|
||||||
@ -674,21 +680,12 @@ export const zCanvasRegionalGuidanceState = z.object({
|
|||||||
});
|
});
|
||||||
export type CanvasRegionalGuidanceState = z.infer<typeof zCanvasRegionalGuidanceState>;
|
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({
|
const zCanvasInpaintMaskState = z.object({
|
||||||
id: z.literal('inpaint_mask'),
|
id: z.literal('inpaint_mask'),
|
||||||
type: z.literal('inpaint_mask'),
|
type: z.literal('inpaint_mask'),
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
position: zCoordinate,
|
position: zCoordinate,
|
||||||
fill: zRgbColor,
|
fill: zFill,
|
||||||
objects: z.array(zCanvasObjectState),
|
objects: z.array(zCanvasObjectState),
|
||||||
rasterizationCache: z.array(zImageCache),
|
rasterizationCache: z.array(zImageCache),
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user