mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add raster layer rendering and interaction (WIP)
This commit is contained in:
parent
f663215f25
commit
d0c40a8b5b
@ -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
|
||||
|
@ -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';
|
@ -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 />
|
||||
|
@ -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,
|
||||
|
@ -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 = () => {
|
||||
|
@ -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()) {
|
||||
|
@ -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}`;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
@ -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 };
|
||||
|
Loading…
Reference in New Issue
Block a user