feat(ui): add raster layer rendering and interaction (WIP)

This commit is contained in:
psychedelicious 2024-06-05 21:24:26 +10:00
parent f663215f25
commit d0c40a8b5b
10 changed files with 529 additions and 218 deletions

View File

@ -182,7 +182,7 @@ const createSelector = (templates: Templates) =>
if (l.type === 'regional_guidance_layer') {
// Must have a region
if (l.maskObjects.length === 0) {
if (l.objects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter

View File

@ -0,0 +1,48 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { brushColorChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const BrushColorPicker = memo(() => {
const { t } = useTranslation();
const brushColor = useAppSelector((s) => s.controlLayers.present.brushColor);
const dispatch = useAppDispatch();
const onChange = useCallback(
(color: RgbaColor) => {
dispatch(brushColorChanged(color));
},
[dispatch]
);
return (
<Popover isLazy>
<PopoverTrigger>
<span>
<Tooltip label={t('controlLayers.brushColor')}>
<Flex
as="button"
aria-label={t('controlLayers.brushColor')}
borderRadius="full"
borderWidth={1}
bg={rgbaColorToString(brushColor)}
w={8}
h={8}
cursor="pointer"
tabIndex={-1}
/>
</Tooltip>
</span>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<IAIColorPicker color={brushColor} onChange={onChange} withNumberInput />
</PopoverBody>
</PopoverContent>
</Popover>
);
});
BrushColorPicker.displayName = 'BrushColorPicker';

View File

@ -1,5 +1,6 @@
/* eslint-disable i18next/no-literal-string */
import { Flex } from '@invoke-ai/ui-library';
import { BrushColorPicker } from 'features/controlLayers/components/BrushColorPicker';
import { BrushSize } from 'features/controlLayers/components/BrushSize';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
@ -18,6 +19,7 @@ export const ControlLayersToolbar = memo(() => {
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<BrushSize />
<BrushColorPicker />
<ToolChooser />
<UndoRedoButtonGroup />
<ControlLayersSettingsPopover />

View File

@ -8,26 +8,32 @@ import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'f
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers';
import {
$brushColor,
$brushSize,
$brushSpacingPx,
$isDrawing,
$lastAddedPoint,
$lastCursorPos,
$lastMouseDownPos,
$selectedLayerId,
$selectedLayerType,
$selectedLayer,
$shouldInvertBrushSizeScrollDirection,
$tool,
brushLineAdded,
brushSizeChanged,
eraserLineAdded,
isRegionalGuidanceLayer,
layerBboxChanged,
layerTranslated,
rgLayerLineAdded,
rgLayerPointsAdded,
rgLayerRectAdded,
linePointsAdded,
rectAdded,
selectControlLayersSlice,
} from 'features/controlLayers/store/controlLayersSlice';
import type { AddLineArg, AddPointToLineArg, AddRectArg } from 'features/controlLayers/store/types';
import type {
AddBrushLineArg,
AddEraserLineArg,
AddPointToLineArg,
AddRectShapeArg,
} from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import { clamp } from 'lodash-es';
@ -41,16 +47,20 @@ Konva.showWarnings = false;
const log = logger('controlLayers');
const selectSelectedLayerColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const selectBrushColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers
.filter(isRegionalGuidanceLayer)
.find((l) => l.id === controlLayers.present.selectedLayerId);
return layer?.previewColor ?? null;
if (layer) {
return { ...layer.previewColor, a: controlLayers.present.globalMaskLayerOpacity };
}
return controlLayers.present.brushColor;
});
const selectSelectedLayerType = createSelector(selectControlLayersSlice, (controlLayers) => {
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
return selectedLayer?.type ?? null;
const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLayers) => {
return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null;
});
const useStageRenderer = (
@ -64,8 +74,8 @@ const useStageRenderer = (
const tool = useStore($tool);
const lastCursorPos = useStore($lastCursorPos);
const lastMouseDownPos = useStore($lastMouseDownPos);
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
const selectedLayerType = useAppSelector(selectSelectedLayerType);
const brushColor = useAppSelector(selectBrushColor);
const selectedLayer = useAppSelector(selectSelectedLayer);
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
const layerCount = useMemo(() => state.layers.length, [state.layers]);
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
@ -77,18 +87,19 @@ const useStageRenderer = (
);
useLayoutEffect(() => {
$brushColor.set(brushColor);
$brushSize.set(state.brushSize);
$brushSpacingPx.set(brushSpacingPx);
$selectedLayerId.set(state.selectedLayerId);
$selectedLayerType.set(selectedLayerType);
$selectedLayer.set(selectedLayer);
$shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection);
}, [
brushSpacingPx,
selectedLayerIdColor,
selectedLayerType,
brushColor,
selectedLayer,
shouldInvertBrushSizeScrollDirection,
state.brushSize,
state.selectedLayerId,
state.brushColor,
]);
const onLayerPosChanged = useCallback(
@ -105,21 +116,27 @@ const useStageRenderer = (
[dispatch]
);
const onRGLayerLineAdded = useCallback(
(arg: AddLineArg) => {
dispatch(rgLayerLineAdded(arg));
const onBrushLineAdded = useCallback(
(arg: AddBrushLineArg) => {
dispatch(brushLineAdded(arg));
},
[dispatch]
);
const onRGLayerPointAddedToLine = useCallback(
const onEraserLineAdded = useCallback(
(arg: AddEraserLineArg) => {
dispatch(eraserLineAdded(arg));
},
[dispatch]
);
const onPointAddedToLine = useCallback(
(arg: AddPointToLineArg) => {
dispatch(rgLayerPointsAdded(arg));
dispatch(linePointsAdded(arg));
},
[dispatch]
);
const onRGLayerRectAdded = useCallback(
(arg: AddRectArg) => {
dispatch(rgLayerRectAdded(arg));
const onRectShapeAdded = useCallback(
(arg: AddRectShapeArg) => {
dispatch(rectAdded(arg));
},
[dispatch]
);
@ -155,21 +172,22 @@ const useStageRenderer = (
$lastCursorPos,
$lastAddedPoint,
$brushSize,
$brushColor,
$brushSpacingPx,
$selectedLayerId,
$selectedLayerType,
$selectedLayer,
$shouldInvertBrushSizeScrollDirection,
onRGLayerLineAdded,
onRGLayerPointAddedToLine,
onRGLayerRectAdded,
onBrushSizeChanged,
onBrushLineAdded,
onEraserLineAdded,
onPointAddedToLine,
onRectShapeAdded,
});
return () => {
log.trace('Removing stage listeners');
cleanup();
};
}, [asPreview, onBrushSizeChanged, onRGLayerLineAdded, onRGLayerPointAddedToLine, onRGLayerRectAdded, stage]);
}, [asPreview, onBrushLineAdded, onBrushSizeChanged, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, stage]);
useLayoutEffect(() => {
log.trace('Updating stage dimensions');
@ -205,8 +223,8 @@ const useStageRenderer = (
renderers.renderToolPreview(
stage,
tool,
selectedLayerIdColor,
selectedLayerType,
brushColor,
selectedLayer?.type ?? null,
state.globalMaskLayerOpacity,
lastCursorPos,
lastMouseDownPos,
@ -216,8 +234,8 @@ const useStageRenderer = (
asPreview,
stage,
tool,
selectedLayerIdColor,
selectedLayerType,
brushColor,
selectedLayer,
state.globalMaskLayerOpacity,
lastCursorPos,
lastMouseDownPos,

View File

@ -15,7 +15,7 @@ import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBol
const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => {
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
return selectedLayer?.type !== 'regional_guidance_layer';
return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer';
});
export const ToolChooser: React.FC = () => {

View File

@ -5,10 +5,19 @@ import {
getScaledFlooredCursorPosition,
snapPosToStage,
} from 'features/controlLayers/konva/util';
import type { AddLineArg, AddPointToLineArg, AddRectArg, Layer, Tool } from 'features/controlLayers/store/types';
import {
type AddBrushLineArg,
type AddEraserLineArg,
type AddPointToLineArg,
type AddRectShapeArg,
DEFAULT_RGBA_COLOR,
type Layer,
type Tool,
} from 'features/controlLayers/store/types';
import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types';
import type { WritableAtom } from 'nanostores';
import type { RgbaColor } from 'react-colorful';
import { TOOL_PREVIEW_LAYER_ID } from './naming';
@ -19,14 +28,15 @@ type SetStageEventHandlersArg = {
$lastMouseDownPos: WritableAtom<Vector2d | null>;
$lastCursorPos: WritableAtom<Vector2d | null>;
$lastAddedPoint: WritableAtom<Vector2d | null>;
$brushColor: WritableAtom<RgbaColor>;
$brushSize: WritableAtom<number>;
$brushSpacingPx: WritableAtom<number>;
$selectedLayerId: WritableAtom<string | null>;
$selectedLayerType: WritableAtom<Layer['type'] | null>;
$selectedLayer: WritableAtom<Layer | null>;
$shouldInvertBrushSizeScrollDirection: WritableAtom<boolean>;
onRGLayerLineAdded: (arg: AddLineArg) => void;
onRGLayerPointAddedToLine: (arg: AddPointToLineArg) => void;
onRGLayerRectAdded: (arg: AddRectArg) => void;
onBrushLineAdded: (arg: AddBrushLineArg) => void;
onEraserLineAdded: (arg: AddEraserLineArg) => void;
onPointAddedToLine: (arg: AddPointToLineArg) => void;
onRectShapeAdded: (arg: AddRectShapeArg) => void;
onBrushSizeChanged: (size: number) => void;
};
@ -46,14 +56,15 @@ export const setStageEventHandlers = ({
$lastMouseDownPos,
$lastCursorPos,
$lastAddedPoint,
$brushColor,
$brushSize,
$brushSpacingPx,
$selectedLayerId,
$selectedLayerType,
$selectedLayer,
$shouldInvertBrushSizeScrollDirection,
onRGLayerLineAdded,
onRGLayerPointAddedToLine,
onRGLayerRectAdded,
onBrushLineAdded,
onEraserLineAdded,
onPointAddedToLine,
onRectShapeAdded,
onBrushSizeChanged,
}: SetStageEventHandlersArg): (() => void) => {
stage.on('mouseenter', (e) => {
@ -72,16 +83,25 @@ export const setStageEventHandlers = ({
}
const tool = $tool.get();
const pos = syncCursorPos(stage, $lastCursorPos);
const selectedLayerId = $selectedLayerId.get();
const selectedLayerType = $selectedLayerType.get();
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
const selectedLayer = $selectedLayer.get();
if (!pos || !selectedLayer) {
return;
}
if (tool === 'brush' || tool === 'eraser') {
onRGLayerLineAdded({
layerId: selectedLayerId,
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
return;
}
if (tool === 'brush') {
onBrushLineAdded({
layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y],
color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR,
});
$isDrawing.set(true);
$lastMouseDownPos.set(pos);
} else if (tool === 'eraser') {
onEraserLineAdded({
layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y],
tool,
});
$isDrawing.set(true);
$lastMouseDownPos.set(pos);
@ -96,24 +116,27 @@ export const setStageEventHandlers = ({
return;
}
const pos = $lastCursorPos.get();
const selectedLayerId = $selectedLayerId.get();
const selectedLayerType = $selectedLayerType.get();
const selectedLayer = $selectedLayer.get();
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
if (!pos || !selectedLayer) {
return;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
return;
}
const lastPos = $lastMouseDownPos.get();
const tool = $tool.get();
if (lastPos && selectedLayerId && tool === 'rect') {
if (lastPos && selectedLayer.id && tool === 'rect') {
const snappedPos = snapPosToStage(pos, stage);
onRGLayerRectAdded({
layerId: selectedLayerId,
onRectShapeAdded({
layerId: selectedLayer.id,
rect: {
x: Math.min(snappedPos.x, lastPos.x),
y: Math.min(snappedPos.y, lastPos.y),
width: Math.abs(snappedPos.x - lastPos.x),
height: Math.abs(snappedPos.y - lastPos.y),
},
color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR,
});
}
$isDrawing.set(false);
@ -127,12 +150,14 @@ export const setStageEventHandlers = ({
}
const tool = $tool.get();
const pos = syncCursorPos(stage, $lastCursorPos);
const selectedLayerId = $selectedLayerId.get();
const selectedLayerType = $selectedLayerType.get();
const selectedLayer = $selectedLayer.get();
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
if (!pos || !selectedLayer) {
return;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
return;
}
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
@ -146,10 +171,21 @@ export const setStageEventHandlers = ({
}
}
$lastAddedPoint.set({ x: pos.x, y: pos.y });
onRGLayerPointAddedToLine({ layerId: selectedLayerId, point: [pos.x, pos.y] });
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
} else {
// Start a new line
onRGLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool });
if (tool === 'brush') {
// Start a new line
onBrushLineAdded({
layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y],
color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR,
});
} else if (tool === 'eraser') {
onEraserLineAdded({
layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y],
});
}
}
$isDrawing.set(true);
}
@ -164,28 +200,36 @@ export const setStageEventHandlers = ({
$isDrawing.set(false);
$lastCursorPos.set(null);
$lastMouseDownPos.set(null);
const selectedLayerId = $selectedLayerId.get();
const selectedLayerType = $selectedLayerType.get();
const selectedLayer = $selectedLayer.get();
const tool = $tool.get();
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false);
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
if (!pos || !selectedLayer) {
return;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
return;
}
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
onRGLayerPointAddedToLine({ layerId: selectedLayerId, point: [pos.x, pos.y] });
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
}
});
stage.on('wheel', (e) => {
e.evt.preventDefault();
const selectedLayerType = $selectedLayerType.get();
const tool = $tool.get();
if (selectedLayerType !== 'regional_guidance_layer' || (tool !== 'brush' && tool !== 'eraser')) {
const selectedLayer = $selectedLayer.get();
if (tool !== 'brush' && tool !== 'eraser') {
return;
}
if (!selectedLayer) {
return;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
return;
}
// Invert the delta if the property is set to true
let delta = e.evt.deltaY;
if ($shouldInvertBrushSizeScrollDirection.get()) {

View File

@ -26,13 +26,17 @@ export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
export const LAYER_BBOX_NAME = 'layer.bbox';
export const COMPOSITING_RECT_NAME = 'compositing-rect';
export const RASTER_LAYER_NAME = 'raster_layer';
export const RASTER_LAYER_LINE_NAME = 'raster_layer.line';
export const RASTER_LAYER_OBJECT_GROUP_NAME = 'raster_layer.object_group';
export const RASTER_LAYER_RECT_NAME = 'raster_layer.rect';
// Getters for non-singleton layer and object IDs
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`;
export const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
export const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
export const getBrushLineId = (layerId: string, lineId: string) => `${layerId}.brush_line_${lineId}`;
export const getEraserLineId = (layerId: string, lineId: string) => `${layerId}.eraser_line_${lineId}`;
export const getRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
export const getObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;

View File

@ -10,11 +10,13 @@ import {
getCALayerImageId,
getIILayerImageId,
getLayerBboxId,
getRGLayerObjectGroupId,
getObjectGroupId,
INITIAL_IMAGE_LAYER_IMAGE_NAME,
INITIAL_IMAGE_LAYER_NAME,
LAYER_BBOX_NAME,
NO_LAYERS_MESSAGE_LAYER_ID,
RASTER_LAYER_NAME,
RASTER_LAYER_OBJECT_GROUP_NAME,
RG_LAYER_LINE_NAME,
RG_LAYER_NAME,
RG_LAYER_OBJECT_GROUP_NAME,
@ -30,6 +32,7 @@ import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/control
import {
isControlAdapterLayer,
isInitialImageLayer,
isRasterLayer,
isRegionalGuidanceLayer,
isRenderableLayer,
} from 'features/controlLayers/store/controlLayersSlice';
@ -39,15 +42,17 @@ import type {
EraserLine,
InitialImageLayer,
Layer,
RasterLayer,
RectShape,
RegionalGuidanceLayer,
RgbaColor,
Tool,
} from 'features/controlLayers/store/types';
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import { t } from 'i18next';
import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types';
import { debounce } from 'lodash-es';
import type { RgbColor } from 'react-colorful';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
@ -59,21 +64,6 @@ import {
TRANSPARENCY_CHECKER_PATTERN,
} from './constants';
const mapId = (object: { id: string }): string => object.id;
/**
* Konva selection callback to select all renderable layers. This includes RG, CA and II layers.
*/
const selectRenderableLayers = (n: Konva.Node): boolean =>
n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME || n.name() === INITIAL_IMAGE_LAYER_NAME;
/**
* Konva selection callback to select RG mask objects. This includes lines and rects.
*/
const selectVectorMaskObjects = (node: Konva.Node): boolean => {
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
};
/**
* Creates the singleton tool preview layer and all its objects.
* @param stage The konva stage
@ -130,7 +120,7 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
const renderToolPreview = (
stage: Konva.Stage,
tool: Tool,
color: RgbColor | null,
brushColor: RgbaColor,
selectedLayerType: Layer['type'] | null,
globalMaskLayerOpacity: number,
cursorPos: Vector2d | null,
@ -142,7 +132,7 @@ const renderToolPreview = (
if (layerCount === 0) {
// We have no layers, so we should not render any tool
stage.container().style.cursor = 'default';
} else if (selectedLayerType !== 'regional_guidance_layer') {
} else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') {
// Non-mask-guidance layers don't have tools
stage.container().style.cursor = 'not-allowed';
} else if (tool === 'move') {
@ -173,14 +163,14 @@ const renderToolPreview = (
assert(rectPreview, 'Rect preview not found');
// No need to render the brush preview if the cursor position or color is missing
if (cursorPos && color && (tool === 'brush' || tool === 'eraser')) {
if (cursorPos && (tool === 'brush' || tool === 'eraser')) {
// Update the fill circle
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`);
brushPreviewFill?.setAttrs({
x: cursorPos.x,
y: cursorPos.y,
radius: brushSize / 2,
fill: rgbaColorToString({ ...color, a: globalMaskLayerOpacity }),
fill: rgbaColorToString(brushColor),
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
});
@ -263,7 +253,7 @@ const createRGLayer = (
// The object group holds all of the layer's objects (e.g. lines and rects)
const konvaObjectGroup = new Konva.Group({
id: getRGLayerObjectGroupId(layerState.id, uuidv4()),
id: getObjectGroupId(layerState.id, uuidv4()),
name: RG_LAYER_OBJECT_GROUP_NAME,
listening: false,
});
@ -273,13 +263,14 @@ const createRGLayer = (
return konvaLayer;
};
//#endregion
/**
* Creates a konva vector mask brush line from a vector mask line.
* @param brushLine The vector mask line state
* Creates a konva line for a brush line.
* @param brushLine The brush line state
* @param layerObjectGroup The konva layer's object group to add the line to
*/
const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => {
const createBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva.Group): Konva.Line => {
const konvaLine = new Konva.Line({
id: brushLine.id,
key: brushLine.id,
@ -291,17 +282,18 @@ const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva
shadowForStrokeEnabled: false,
globalCompositeOperation: 'source-over',
listening: false,
stroke: rgbaColorToString(brushLine.color),
});
layerObjectGroup.add(konvaLine);
return konvaLine;
};
/**
* Creates a konva vector mask eraser line from a vector mask line.
* @param eraserLine The vector mask line state
* Creates a konva line for a eraser line.
* @param eraserLine The eraser line state
* @param layerObjectGroup The konva layer's object group to add the line to
*/
const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => {
const createEraserLine = (eraserLine: EraserLine, layerObjectGroup: Konva.Group): Konva.Line => {
const konvaLine = new Konva.Line({
id: eraserLine.id,
key: eraserLine.id,
@ -313,42 +305,35 @@ const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Ko
shadowForStrokeEnabled: false,
globalCompositeOperation: 'destination-out',
listening: false,
stroke: rgbaColorToString(DEFAULT_RGBA_COLOR),
});
layerObjectGroup.add(konvaLine);
return konvaLine;
};
const createVectorMaskLine = (maskObject: BrushLine | EraserLine, layerObjectGroup: Konva.Group): Konva.Line => {
if (maskObject.type === 'brush_line') {
return createVectorMaskBrushLine(maskObject, layerObjectGroup);
} else {
// maskObject.type === 'eraser_line'
return createVectorMaskEraserLine(maskObject, layerObjectGroup);
}
};
/**
* Creates a konva rect from a vector mask rect.
* @param vectorMaskRect The vector mask rect state
* Creates a konva rect for a rect shape.
* @param rectShape The rect shape state
* @param layerObjectGroup The konva layer's object group to add the line to
*/
const createVectorMaskRect = (vectorMaskRect: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => {
const createRectShape = (rectShape: RectShape, layerObjectGroup: Konva.Group): Konva.Rect => {
const konvaRect = new Konva.Rect({
id: vectorMaskRect.id,
key: vectorMaskRect.id,
id: rectShape.id,
key: rectShape.id,
name: RG_LAYER_RECT_NAME,
x: vectorMaskRect.x,
y: vectorMaskRect.y,
width: vectorMaskRect.width,
height: vectorMaskRect.height,
x: rectShape.x,
y: rectShape.y,
width: rectShape.width,
height: rectShape.height,
listening: false,
fill: rgbaColorToString(rectShape.color),
});
layerObjectGroup.add(konvaRect);
return konvaRect;
};
/**
* Creates the "compositing rect" for a layer.
* Creates the "compositing rect" for a regional guidance layer.
* @param konvaLayer The konva layer
*/
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
@ -358,7 +343,7 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
};
/**
* Renders a regional guidance layer.
* Renders a raster layer.
* @param stage The konva stage
* @param layerState The regional guidance layer state
* @param globalMaskLayerOpacity The global mask layer opacity
@ -391,7 +376,7 @@ const renderRGLayer = (
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
let groupNeedsCache = false;
const objectIds = layerState.maskObjects.map(mapId);
const objectIds = layerState.objects.map(mapId);
// Destroy any objects that are no longer in the redux state
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
if (!objectIds.includes(objectNode.id())) {
@ -400,29 +385,41 @@ const renderRGLayer = (
}
}
for (const maskObject of layerState.maskObjects) {
if (maskObject.type === 'brush_line' || maskObject.type === 'eraser_line') {
const vectorMaskLine =
stage.findOne<Konva.Line>(`#${maskObject.id}`) ?? createVectorMaskLine(maskObject, konvaObjectGroup);
for (const obj of layerState.objects) {
if (obj.type === 'brush_line') {
const konvaBrushLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
if (vectorMaskLine.points().length !== maskObject.points.length) {
vectorMaskLine.points(maskObject.points);
if (konvaBrushLine.points().length !== obj.points.length) {
konvaBrushLine.points(obj.points);
groupNeedsCache = true;
}
// Only update the color if it has changed.
if (vectorMaskLine.stroke() !== rgbColor) {
vectorMaskLine.stroke(rgbColor);
if (konvaBrushLine.stroke() !== rgbColor) {
konvaBrushLine.stroke(rgbColor);
groupNeedsCache = true;
}
} else if (maskObject.type === 'rect_shape') {
const konvaObject =
stage.findOne<Konva.Rect>(`#${maskObject.id}`) ?? createVectorMaskRect(maskObject, konvaObjectGroup);
} else if (obj.type === 'eraser_line') {
const konvaEraserLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup);
// Only update the points if they have changed. The point values are never mutated, they are only added to the
// array, so checking the length is sufficient to determine if we need to re-cache.
if (konvaEraserLine.points().length !== obj.points.length) {
konvaEraserLine.points(obj.points);
groupNeedsCache = true;
}
// Only update the color if it has changed.
if (konvaEraserLine.stroke() !== rgbColor) {
konvaEraserLine.stroke(rgbColor);
groupNeedsCache = true;
}
} else if (obj.type === 'rect_shape') {
const konvaRectShape = stage.findOne<Konva.Rect>(`#${obj.id}`) ?? createRectShape(obj, konvaObjectGroup);
// Only update the color if it has changed.
if (konvaObject.fill() !== rgbColor) {
konvaObject.fill(rgbColor);
if (konvaRectShape.fill() !== rgbColor) {
konvaRectShape.fill(rgbColor);
groupNeedsCache = true;
}
}
@ -485,6 +482,126 @@ const renderRGLayer = (
}
};
/**
* Creates a raster layer.
* @param stage The konva stage
* @param layerState The raster layer state
* @param onLayerPosChanged Callback for when the layer's position changes
*/
const createRasterLayer = (
stage: Konva.Stage,
layerState: RasterLayer,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
): Konva.Layer => {
// This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({
id: layerState.id,
name: RASTER_LAYER_NAME,
draggable: true,
dragDistance: 0,
});
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
// the position - we do not need to call this on the `dragmove` event.
if (onLayerPosChanged) {
konvaLayer.on('dragend', function (e) {
onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y()));
});
}
// The dragBoundFunc limits how far the layer can be dragged
konvaLayer.dragBoundFunc(function (pos) {
const cursorPos = getScaledFlooredCursorPosition(stage);
if (!cursorPos) {
return this.getAbsolutePosition();
}
// Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds
if (
cursorPos.x < 0 ||
cursorPos.x > stage.width() / stage.scaleX() ||
cursorPos.y < 0 ||
cursorPos.y > stage.height() / stage.scaleY()
) {
return this.getAbsolutePosition();
}
return pos;
});
// The object group holds all of the layer's objects (e.g. lines and rects)
const konvaObjectGroup = new Konva.Group({
id: getObjectGroupId(layerState.id, uuidv4()),
name: RASTER_LAYER_OBJECT_GROUP_NAME,
listening: false,
});
konvaLayer.add(konvaObjectGroup);
stage.add(konvaLayer);
return konvaLayer;
};
/**
* Renders a regional guidance layer.
* @param stage The konva stage
* @param layerState The regional guidance layer state
* @param tool The current tool
* @param onLayerPosChanged Callback for when the layer's position changes
*/
const renderRasterLayer = (
stage: Konva.Stage,
layerState: RasterLayer,
tool: Tool,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
): void => {
const konvaLayer =
stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onLayerPosChanged);
// Update the layer's position and listening state
konvaLayer.setAttrs({
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
x: Math.floor(layerState.x),
y: Math.floor(layerState.y),
});
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${RASTER_LAYER_OBJECT_GROUP_NAME}`);
assert(konvaObjectGroup, `Object group not found for layer ${layerState.id}`);
const objectIds = layerState.objects.map(mapId);
// Destroy any objects that are no longer in the redux state
for (const objectNode of konvaObjectGroup.getChildren()) {
if (!objectIds.includes(objectNode.id())) {
objectNode.destroy();
}
}
for (const obj of layerState.objects) {
if (obj.type === 'brush_line') {
const konvaBrushLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup);
// Only update the points if they have changed.
if (konvaBrushLine.points().length !== obj.points.length) {
konvaBrushLine.points(obj.points);
}
} else if (obj.type === 'eraser_line') {
const konvaEraserLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup);
// Only update the points if they have changed.
if (konvaEraserLine.points().length !== obj.points.length) {
konvaEraserLine.points(obj.points);
}
} else if (obj.type === 'rect_shape') {
if (!stage.findOne<Konva.Rect>(`#${obj.id}`)) {
createRectShape(obj, konvaObjectGroup);
}
}
}
// Only update layer visibility if it has changed.
if (konvaLayer.visible() !== layerState.isEnabled) {
konvaLayer.visible(layerState.isEnabled);
}
konvaObjectGroup.opacity(layerState.opacity);
};
/**
* Creates an initial image konva layer.
* @param stage The konva stage
@ -805,6 +922,9 @@ const renderLayers = (
if (isInitialImageLayer(layer)) {
renderIILayer(stage, layer, getImageDTO);
}
if (isRasterLayer(layer)) {
renderRasterLayer(stage, layer, tool, onLayerPosChanged);
}
// IP Adapter layers are not rendered
}
};
@ -886,7 +1006,7 @@ const updateBboxes = (
const visible = bboxRect.visible();
bboxRect.visible(false);
if (rgLayer.maskObjects.length === 0) {
if (rgLayer.objects.length === 0) {
// No objects - no bbox to calculate
onBboxChanged(rgLayer.id, null);
} else {
@ -1041,3 +1161,23 @@ export const debouncedRenderers = {
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
updateBboxes: debounce(updateBboxes, DEBOUNCE_MS),
};
//#region util
const mapId = (object: { id: string }): string => object.id;
/**
* Konva selection callback to select all renderable layers. This includes RG, CA and II layers.
*/
const selectRenderableLayers = (n: Konva.Node): boolean =>
n.name() === RG_LAYER_NAME ||
n.name() === CA_LAYER_NAME ||
n.name() === INITIAL_IMAGE_LAYER_NAME ||
n.name() === RASTER_LAYER_NAME;
/**
* Konva selection callback to select RG mask objects. This includes lines and rects.
*/
const selectVectorMaskObjects = (node: Konva.Node): boolean => {
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
};
//#endregion

View File

@ -5,12 +5,13 @@ import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
import {
getBrushLineId,
getCALayerId,
getEraserLineId,
getIPALayerId,
getRasterLayerId,
getRectId,
getRGLayerId,
getRGLayerLineId,
getRGLayerRectId,
INITIAL_IMAGE_LAYER_ID,
} from 'features/controlLayers/konva/naming';
import type {
@ -45,20 +46,24 @@ import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
AddLineArg,
AddBrushLineArg,
AddEraserLineArg,
AddPointToLineArg,
AddRectArg,
AddRectShapeArg,
BrushLine,
ControlAdapterLayer,
ControlLayersState,
DrawingTool,
EllipseShape,
EraserLine,
ImageObject,
InitialImageLayer,
IPAdapterLayer,
Layer,
PolygonShape,
RasterLayer,
RectShape,
RegionalGuidanceLayer,
RgbaColor,
Tool,
} from './types';
import { DEFAULT_RGBA_COLOR } from './types';
@ -67,6 +72,7 @@ export const initialControlLayersState: ControlLayersState = {
_version: 3,
selectedLayerId: null,
brushSize: 100,
brushColor: DEFAULT_RGBA_COLOR,
layers: [],
globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity
positivePrompt: '',
@ -81,8 +87,9 @@ export const initialControlLayersState: ControlLayersState = {
},
};
const isLine = (obj: BrushLine | EraserLine | RectShape): obj is BrushLine | EraserLine =>
obj.type === 'brush_line' || obj.type === 'eraser_line';
const isLine = (
obj: BrushLine | EraserLine | RectShape | EllipseShape | PolygonShape | ImageObject
): obj is BrushLine => obj.type === 'brush_line' || obj.type === 'eraser_line';
export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer =>
layer?.type === 'regional_guidance_layer';
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer =>
@ -131,6 +138,14 @@ const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): Regio
assert(isRegionalGuidanceLayer(layer));
return layer;
};
const selectRGOrRasterLayerOrThrow = (
state: ControlLayersState,
layerId: string
): RegionalGuidanceLayer | RasterLayer => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer) || isRasterLayer(layer));
return layer;
};
export const selectRGLayerIPAdapterOrThrow = (
state: ControlLayersState,
layerId: string,
@ -187,7 +202,7 @@ export const controlLayersSlice = createSlice({
layer.bboxNeedsUpdate = false;
if (bbox === null && layer.type === 'regional_guidance_layer') {
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
layer.maskObjects = [];
layer.objects = [];
layer.uploadedMaskImage = null;
}
}
@ -196,7 +211,7 @@ export const controlLayersSlice = createSlice({
const layer = state.layers.find((l) => l.id === action.payload);
// TODO(psyche): Should other layer types also have reset functionality?
if (isRegionalGuidanceLayer(layer)) {
layer.maskObjects = [];
layer.objects = [];
layer.bbox = null;
layer.isEnabled = true;
layer.bboxNeedsUpdate = false;
@ -455,7 +470,7 @@ export const controlLayersSlice = createSlice({
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
maskObjects: [],
objects: [],
previewColor: getVectorMaskPreviewColor(state),
x: 0,
y: 0,
@ -490,81 +505,102 @@ export const controlLayersSlice = createSlice({
const layer = selectRGLayerOrThrow(state, layerId);
layer.previewColor = color;
},
rgLayerLineAdded: {
brushLineAdded: {
reducer: (
state,
action: PayloadAction<{
layerId: string;
points: [number, number, number, number];
tool: DrawingTool;
lineUuid: string;
}>
action: PayloadAction<
AddBrushLineArg & {
lineUuid: string;
}
>
) => {
const { layerId, points, tool, lineUuid } = action.payload;
const layer = selectRGLayerOrThrow(state, layerId);
const lineId = getRGLayerLineId(layer.id, lineUuid);
if (tool === 'brush') {
layer.maskObjects.push({
id: lineId,
type: 'brush_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
strokeWidth: state.brushSize,
color: DEFAULT_RGBA_COLOR,
});
} else {
layer.maskObjects.push({
id: lineId,
type: 'eraser_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
strokeWidth: state.brushSize,
});
}
const { layerId, points, lineUuid, color } = action.payload;
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
layer.objects.push({
id: getBrushLineId(layer.id, lineUuid),
type: 'brush_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
strokeWidth: state.brushSize,
color,
});
layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
if (layer.type === 'regional_guidance_layer') {
layer.uploadedMaskImage = null;
}
},
prepare: (payload: AddLineArg) => ({
prepare: (payload: AddBrushLineArg) => ({
payload: { ...payload, lineUuid: uuidv4() },
}),
},
rgLayerPointsAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
eraserLineAdded: {
reducer: (
state,
action: PayloadAction<
AddEraserLineArg & {
lineUuid: string;
}
>
) => {
const { layerId, points, lineUuid } = action.payload;
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
layer.objects.push({
id: getEraserLineId(layer.id, lineUuid),
type: 'eraser_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
strokeWidth: state.brushSize,
});
layer.bboxNeedsUpdate = true;
if (isRegionalGuidanceLayer(layer)) {
layer.uploadedMaskImage = null;
}
},
prepare: (payload: AddEraserLineArg) => ({
payload: { ...payload, lineUuid: uuidv4() },
}),
},
linePointsAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
const { layerId, point } = action.payload;
const layer = selectRGLayerOrThrow(state, layerId);
const lastLine = layer.maskObjects.findLast(isLine);
if (!lastLine) {
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
const lastLine = layer.objects.findLast(isLine);
if (!lastLine || !isLine(lastLine)) {
return;
}
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
if (isRegionalGuidanceLayer(layer)) {
layer.uploadedMaskImage = null;
}
},
rgLayerRectAdded: {
reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => {
const { layerId, rect, rectUuid } = action.payload;
rectAdded: {
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectUuid: string }>) => {
const { layerId, rect, rectUuid, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
return;
}
const layer = selectRGLayerOrThrow(state, layerId);
const id = getRGLayerRectId(layer.id, rectUuid);
layer.maskObjects.push({
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
const id = getRectId(layer.id, rectUuid);
layer.objects.push({
type: 'rect_shape',
id,
x: rect.x - layer.x,
y: rect.y - layer.y,
width: rect.width,
height: rect.height,
color: DEFAULT_RGBA_COLOR,
color,
});
layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
if (isRegionalGuidanceLayer(layer)) {
layer.uploadedMaskImage = null;
}
},
prepare: (payload: AddRectArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }),
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }),
},
rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => {
const { layerId, imageDTO } = action.payload;
@ -776,6 +812,9 @@ export const controlLayersSlice = createSlice({
brushSizeChanged: (state, action: PayloadAction<number>) => {
state.brushSize = Math.round(action.payload);
},
brushColorChanged: (state, action: PayloadAction<RgbaColor>) => {
state.brushColor = action.payload;
},
globalMaskLayerOpacityChanged: (state, action: PayloadAction<number>) => {
state.globalMaskLayerOpacity = action.payload;
},
@ -892,9 +931,10 @@ export const {
rgLayerPositivePromptChanged,
rgLayerNegativePromptChanged,
rgLayerPreviewColorChanged,
rgLayerLineAdded,
rgLayerPointsAdded,
rgLayerRectAdded,
brushLineAdded,
eraserLineAdded,
linePointsAdded,
rectAdded,
rgLayerMaskImageUploaded,
rgLayerAutoNegativeChanged,
rgLayerIPAdapterAdded,
@ -924,6 +964,7 @@ export const {
heightChanged,
aspectRatioChanged,
brushSizeChanged,
brushColorChanged,
globalMaskLayerOpacityChanged,
undo,
redo,
@ -960,9 +1001,9 @@ export const $lastAddedPoint = atom<Vector2d | null>(null);
// Some nanostores that are manually synced to redux state to provide imperative access
// TODO(psyche): This is a hack, figure out another way to handle this...
export const $brushSize = atom<number>(0);
export const $brushColor = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
export const $brushSpacingPx = atom<number>(0);
export const $selectedLayerId = atom<string | null>(null);
export const $selectedLayerType = atom<Layer['type'] | null>(null);
export const $selectedLayer = atom<Layer | null>(null);
export const $shouldInvertBrushSizeScrollDirection = atom(false);
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
@ -998,10 +1039,10 @@ export const controlLayersUndoableConfig: UndoableOptions<ControlLayersState, Un
// Lines are started with `rgLayerLineAdded` and may have any number of subsequent `rgLayerPointsAdded` events.
// We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping
// separate logical lines as a single undo action.
if (rgLayerLineAdded.match(action)) {
if (brushLineAdded.match(action)) {
return history.group === LINE_1 ? LINE_2 : LINE_1;
}
if (rgLayerPointsAdded.match(action)) {
if (linePointsAdded.match(action)) {
if (history.group === LINE_1 || history.group === LINE_2) {
return history.group;
}
@ -1012,15 +1053,6 @@ export const controlLayersUndoableConfig: UndoableOptions<ControlLayersState, Un
return null;
},
filter: (action, _state, _history) => {
// Ignore all actions from other slices
if (!action.type.startsWith(controlLayersSlice.name)) {
return false;
}
// This action is triggered on state changes, including when we undo. If we do not ignore this action, when we
// undo, this action triggers and empties the future states array. Therefore, we must ignore this action.
if (layerBboxChanged.match(action)) {
return false;
}
return true;
return false;
},
};

View File

@ -55,7 +55,7 @@ const zRgbColor = z.object({
const zRgbaColor = zRgbColor.extend({
a: z.number().min(0).max(1),
});
type RgbaColor = z.infer<typeof zRgbaColor>;
export type RgbaColor = z.infer<typeof zRgbaColor>;
export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };
const zOpacity = z.number().gte(0).lte(1);
@ -193,7 +193,7 @@ const zMaskObject = z
})
.pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape]));
const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({
type: z.literal('regional_guidance_layer'),
maskObjects: z.array(zMaskObject),
positivePrompt: zParameterPositivePrompt.nullable(),
@ -203,7 +203,28 @@ const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
autoNegative: zAutoNegative,
uploadedMaskImage: zImageWithDims.nullable(),
});
export type RegionalGuidanceLayer = z.infer<typeof zRegionalGuidanceLayer>;
const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
type: z.literal('regional_guidance_layer'),
objects: z.array(zMaskObject),
positivePrompt: zParameterPositivePrompt.nullable(),
negativePrompt: zParameterNegativePrompt.nullable(),
ipAdapters: z.array(zIPAdapterConfigV2),
previewColor: zRgbColor,
autoNegative: zAutoNegative,
uploadedMaskImage: zImageWithDims.nullable(),
});
const zRGLayer = z
.union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer])
.transform((val) => {
if ('maskObjects' in val) {
const { maskObjects, ...rest } = val;
return { ...rest, objects: maskObjects };
} else {
return val;
}
})
.pipe(zRegionalGuidanceLayer);
export type RegionalGuidanceLayer = z.infer<typeof zRGLayer>;
const zInitialImageLayer = zRenderableLayerBase.extend({
type: z.literal('initial_image_layer'),
@ -227,6 +248,7 @@ export type ControlLayersState = {
selectedLayerId: string | null;
layers: Layer[];
brushSize: number;
brushColor: RgbaColor;
globalMaskLayerOpacity: number;
positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt;
@ -240,6 +262,7 @@ export type ControlLayersState = {
};
};
export type AddLineArg = { layerId: string; points: [number, number, number, number]; tool: DrawingTool };
export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] };
export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor };
export type AddPointToLineArg = { layerId: string; point: [number, number] };
export type AddRectArg = { layerId: string; rect: IRect };
export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor };