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') {
|
if (l.type === 'regional_guidance_layer') {
|
||||||
// Must have a region
|
// Must have a region
|
||||||
if (l.maskObjects.length === 0) {
|
if (l.objects.length === 0) {
|
||||||
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
||||||
}
|
}
|
||||||
// Must have at least 1 prompt or IP Adapter
|
// 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 */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { BrushColorPicker } from 'features/controlLayers/components/BrushColorPicker';
|
||||||
import { BrushSize } from 'features/controlLayers/components/BrushSize';
|
import { BrushSize } from 'features/controlLayers/components/BrushSize';
|
||||||
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
||||||
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
||||||
@ -18,6 +19,7 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
<BrushSize />
|
<BrushSize />
|
||||||
|
<BrushColorPicker />
|
||||||
<ToolChooser />
|
<ToolChooser />
|
||||||
<UndoRedoButtonGroup />
|
<UndoRedoButtonGroup />
|
||||||
<ControlLayersSettingsPopover />
|
<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 { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
||||||
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers';
|
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers';
|
||||||
import {
|
import {
|
||||||
|
$brushColor,
|
||||||
$brushSize,
|
$brushSize,
|
||||||
$brushSpacingPx,
|
$brushSpacingPx,
|
||||||
$isDrawing,
|
$isDrawing,
|
||||||
$lastAddedPoint,
|
$lastAddedPoint,
|
||||||
$lastCursorPos,
|
$lastCursorPos,
|
||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
$selectedLayerId,
|
$selectedLayer,
|
||||||
$selectedLayerType,
|
|
||||||
$shouldInvertBrushSizeScrollDirection,
|
$shouldInvertBrushSizeScrollDirection,
|
||||||
$tool,
|
$tool,
|
||||||
|
brushLineAdded,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
|
eraserLineAdded,
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
layerTranslated,
|
layerTranslated,
|
||||||
rgLayerLineAdded,
|
linePointsAdded,
|
||||||
rgLayerPointsAdded,
|
rectAdded,
|
||||||
rgLayerRectAdded,
|
|
||||||
selectControlLayersSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} 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 Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
@ -41,16 +47,20 @@ Konva.showWarnings = false;
|
|||||||
|
|
||||||
const log = logger('controlLayers');
|
const log = logger('controlLayers');
|
||||||
|
|
||||||
const selectSelectedLayerColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
const selectBrushColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const layer = controlLayers.present.layers
|
const layer = controlLayers.present.layers
|
||||||
.filter(isRegionalGuidanceLayer)
|
.filter(isRegionalGuidanceLayer)
|
||||||
.find((l) => l.id === controlLayers.present.selectedLayerId);
|
.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 selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
|
return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null;
|
||||||
return selectedLayer?.type ?? null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const useStageRenderer = (
|
const useStageRenderer = (
|
||||||
@ -64,8 +74,8 @@ const useStageRenderer = (
|
|||||||
const tool = useStore($tool);
|
const tool = useStore($tool);
|
||||||
const lastCursorPos = useStore($lastCursorPos);
|
const lastCursorPos = useStore($lastCursorPos);
|
||||||
const lastMouseDownPos = useStore($lastMouseDownPos);
|
const lastMouseDownPos = useStore($lastMouseDownPos);
|
||||||
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
|
const brushColor = useAppSelector(selectBrushColor);
|
||||||
const selectedLayerType = useAppSelector(selectSelectedLayerType);
|
const selectedLayer = useAppSelector(selectSelectedLayer);
|
||||||
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
|
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
|
||||||
const layerCount = useMemo(() => state.layers.length, [state.layers]);
|
const layerCount = useMemo(() => state.layers.length, [state.layers]);
|
||||||
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
||||||
@ -77,18 +87,19 @@ const useStageRenderer = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
$brushColor.set(brushColor);
|
||||||
$brushSize.set(state.brushSize);
|
$brushSize.set(state.brushSize);
|
||||||
$brushSpacingPx.set(brushSpacingPx);
|
$brushSpacingPx.set(brushSpacingPx);
|
||||||
$selectedLayerId.set(state.selectedLayerId);
|
$selectedLayer.set(selectedLayer);
|
||||||
$selectedLayerType.set(selectedLayerType);
|
|
||||||
$shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection);
|
$shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection);
|
||||||
}, [
|
}, [
|
||||||
brushSpacingPx,
|
brushSpacingPx,
|
||||||
selectedLayerIdColor,
|
brushColor,
|
||||||
selectedLayerType,
|
selectedLayer,
|
||||||
shouldInvertBrushSizeScrollDirection,
|
shouldInvertBrushSizeScrollDirection,
|
||||||
state.brushSize,
|
state.brushSize,
|
||||||
state.selectedLayerId,
|
state.selectedLayerId,
|
||||||
|
state.brushColor,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onLayerPosChanged = useCallback(
|
const onLayerPosChanged = useCallback(
|
||||||
@ -105,21 +116,27 @@ const useStageRenderer = (
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onRGLayerLineAdded = useCallback(
|
const onBrushLineAdded = useCallback(
|
||||||
(arg: AddLineArg) => {
|
(arg: AddBrushLineArg) => {
|
||||||
dispatch(rgLayerLineAdded(arg));
|
dispatch(brushLineAdded(arg));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
const onRGLayerPointAddedToLine = useCallback(
|
const onEraserLineAdded = useCallback(
|
||||||
|
(arg: AddEraserLineArg) => {
|
||||||
|
dispatch(eraserLineAdded(arg));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
const onPointAddedToLine = useCallback(
|
||||||
(arg: AddPointToLineArg) => {
|
(arg: AddPointToLineArg) => {
|
||||||
dispatch(rgLayerPointsAdded(arg));
|
dispatch(linePointsAdded(arg));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
const onRGLayerRectAdded = useCallback(
|
const onRectShapeAdded = useCallback(
|
||||||
(arg: AddRectArg) => {
|
(arg: AddRectShapeArg) => {
|
||||||
dispatch(rgLayerRectAdded(arg));
|
dispatch(rectAdded(arg));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -155,21 +172,22 @@ const useStageRenderer = (
|
|||||||
$lastCursorPos,
|
$lastCursorPos,
|
||||||
$lastAddedPoint,
|
$lastAddedPoint,
|
||||||
$brushSize,
|
$brushSize,
|
||||||
|
$brushColor,
|
||||||
$brushSpacingPx,
|
$brushSpacingPx,
|
||||||
$selectedLayerId,
|
$selectedLayer,
|
||||||
$selectedLayerType,
|
|
||||||
$shouldInvertBrushSizeScrollDirection,
|
$shouldInvertBrushSizeScrollDirection,
|
||||||
onRGLayerLineAdded,
|
|
||||||
onRGLayerPointAddedToLine,
|
|
||||||
onRGLayerRectAdded,
|
|
||||||
onBrushSizeChanged,
|
onBrushSizeChanged,
|
||||||
|
onBrushLineAdded,
|
||||||
|
onEraserLineAdded,
|
||||||
|
onPointAddedToLine,
|
||||||
|
onRectShapeAdded,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
log.trace('Removing stage listeners');
|
log.trace('Removing stage listeners');
|
||||||
cleanup();
|
cleanup();
|
||||||
};
|
};
|
||||||
}, [asPreview, onBrushSizeChanged, onRGLayerLineAdded, onRGLayerPointAddedToLine, onRGLayerRectAdded, stage]);
|
}, [asPreview, onBrushLineAdded, onBrushSizeChanged, onEraserLineAdded, onPointAddedToLine, onRectShapeAdded, stage]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Updating stage dimensions');
|
log.trace('Updating stage dimensions');
|
||||||
@ -205,8 +223,8 @@ const useStageRenderer = (
|
|||||||
renderers.renderToolPreview(
|
renderers.renderToolPreview(
|
||||||
stage,
|
stage,
|
||||||
tool,
|
tool,
|
||||||
selectedLayerIdColor,
|
brushColor,
|
||||||
selectedLayerType,
|
selectedLayer?.type ?? null,
|
||||||
state.globalMaskLayerOpacity,
|
state.globalMaskLayerOpacity,
|
||||||
lastCursorPos,
|
lastCursorPos,
|
||||||
lastMouseDownPos,
|
lastMouseDownPos,
|
||||||
@ -216,8 +234,8 @@ const useStageRenderer = (
|
|||||||
asPreview,
|
asPreview,
|
||||||
stage,
|
stage,
|
||||||
tool,
|
tool,
|
||||||
selectedLayerIdColor,
|
brushColor,
|
||||||
selectedLayerType,
|
selectedLayer,
|
||||||
state.globalMaskLayerOpacity,
|
state.globalMaskLayerOpacity,
|
||||||
lastCursorPos,
|
lastCursorPos,
|
||||||
lastMouseDownPos,
|
lastMouseDownPos,
|
||||||
|
@ -15,7 +15,7 @@ import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBol
|
|||||||
|
|
||||||
const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => {
|
const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
|
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 = () => {
|
export const ToolChooser: React.FC = () => {
|
||||||
|
@ -5,10 +5,19 @@ import {
|
|||||||
getScaledFlooredCursorPosition,
|
getScaledFlooredCursorPosition,
|
||||||
snapPosToStage,
|
snapPosToStage,
|
||||||
} from 'features/controlLayers/konva/util';
|
} 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 Konva from 'konva';
|
||||||
import type { Vector2d } from 'konva/lib/types';
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
import type { WritableAtom } from 'nanostores';
|
import type { WritableAtom } from 'nanostores';
|
||||||
|
import type { RgbaColor } from 'react-colorful';
|
||||||
|
|
||||||
import { TOOL_PREVIEW_LAYER_ID } from './naming';
|
import { TOOL_PREVIEW_LAYER_ID } from './naming';
|
||||||
|
|
||||||
@ -19,14 +28,15 @@ type SetStageEventHandlersArg = {
|
|||||||
$lastMouseDownPos: WritableAtom<Vector2d | null>;
|
$lastMouseDownPos: WritableAtom<Vector2d | null>;
|
||||||
$lastCursorPos: WritableAtom<Vector2d | null>;
|
$lastCursorPos: WritableAtom<Vector2d | null>;
|
||||||
$lastAddedPoint: WritableAtom<Vector2d | null>;
|
$lastAddedPoint: WritableAtom<Vector2d | null>;
|
||||||
|
$brushColor: WritableAtom<RgbaColor>;
|
||||||
$brushSize: WritableAtom<number>;
|
$brushSize: WritableAtom<number>;
|
||||||
$brushSpacingPx: WritableAtom<number>;
|
$brushSpacingPx: WritableAtom<number>;
|
||||||
$selectedLayerId: WritableAtom<string | null>;
|
$selectedLayer: WritableAtom<Layer | null>;
|
||||||
$selectedLayerType: WritableAtom<Layer['type'] | null>;
|
|
||||||
$shouldInvertBrushSizeScrollDirection: WritableAtom<boolean>;
|
$shouldInvertBrushSizeScrollDirection: WritableAtom<boolean>;
|
||||||
onRGLayerLineAdded: (arg: AddLineArg) => void;
|
onBrushLineAdded: (arg: AddBrushLineArg) => void;
|
||||||
onRGLayerPointAddedToLine: (arg: AddPointToLineArg) => void;
|
onEraserLineAdded: (arg: AddEraserLineArg) => void;
|
||||||
onRGLayerRectAdded: (arg: AddRectArg) => void;
|
onPointAddedToLine: (arg: AddPointToLineArg) => void;
|
||||||
|
onRectShapeAdded: (arg: AddRectShapeArg) => void;
|
||||||
onBrushSizeChanged: (size: number) => void;
|
onBrushSizeChanged: (size: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,14 +56,15 @@ export const setStageEventHandlers = ({
|
|||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
$lastCursorPos,
|
$lastCursorPos,
|
||||||
$lastAddedPoint,
|
$lastAddedPoint,
|
||||||
|
$brushColor,
|
||||||
$brushSize,
|
$brushSize,
|
||||||
$brushSpacingPx,
|
$brushSpacingPx,
|
||||||
$selectedLayerId,
|
$selectedLayer,
|
||||||
$selectedLayerType,
|
|
||||||
$shouldInvertBrushSizeScrollDirection,
|
$shouldInvertBrushSizeScrollDirection,
|
||||||
onRGLayerLineAdded,
|
onBrushLineAdded,
|
||||||
onRGLayerPointAddedToLine,
|
onEraserLineAdded,
|
||||||
onRGLayerRectAdded,
|
onPointAddedToLine,
|
||||||
|
onRectShapeAdded,
|
||||||
onBrushSizeChanged,
|
onBrushSizeChanged,
|
||||||
}: SetStageEventHandlersArg): (() => void) => {
|
}: SetStageEventHandlersArg): (() => void) => {
|
||||||
stage.on('mouseenter', (e) => {
|
stage.on('mouseenter', (e) => {
|
||||||
@ -72,16 +83,25 @@ export const setStageEventHandlers = ({
|
|||||||
}
|
}
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
const pos = syncCursorPos(stage, $lastCursorPos);
|
const pos = syncCursorPos(stage, $lastCursorPos);
|
||||||
const selectedLayerId = $selectedLayerId.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
const selectedLayerType = $selectedLayerType.get();
|
if (!pos || !selectedLayer) {
|
||||||
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (tool === 'brush' || tool === 'eraser') {
|
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
||||||
onRGLayerLineAdded({
|
return;
|
||||||
layerId: selectedLayerId,
|
}
|
||||||
|
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],
|
points: [pos.x, pos.y, pos.x, pos.y],
|
||||||
tool,
|
|
||||||
});
|
});
|
||||||
$isDrawing.set(true);
|
$isDrawing.set(true);
|
||||||
$lastMouseDownPos.set(pos);
|
$lastMouseDownPos.set(pos);
|
||||||
@ -96,24 +116,27 @@ export const setStageEventHandlers = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pos = $lastCursorPos.get();
|
const pos = $lastCursorPos.get();
|
||||||
const selectedLayerId = $selectedLayerId.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
const selectedLayerType = $selectedLayerType.get();
|
|
||||||
|
|
||||||
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
|
if (!pos || !selectedLayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lastPos = $lastMouseDownPos.get();
|
const lastPos = $lastMouseDownPos.get();
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
if (lastPos && selectedLayerId && tool === 'rect') {
|
if (lastPos && selectedLayer.id && tool === 'rect') {
|
||||||
const snappedPos = snapPosToStage(pos, stage);
|
const snappedPos = snapPosToStage(pos, stage);
|
||||||
onRGLayerRectAdded({
|
onRectShapeAdded({
|
||||||
layerId: selectedLayerId,
|
layerId: selectedLayer.id,
|
||||||
rect: {
|
rect: {
|
||||||
x: Math.min(snappedPos.x, lastPos.x),
|
x: Math.min(snappedPos.x, lastPos.x),
|
||||||
y: Math.min(snappedPos.y, lastPos.y),
|
y: Math.min(snappedPos.y, lastPos.y),
|
||||||
width: Math.abs(snappedPos.x - lastPos.x),
|
width: Math.abs(snappedPos.x - lastPos.x),
|
||||||
height: Math.abs(snappedPos.y - lastPos.y),
|
height: Math.abs(snappedPos.y - lastPos.y),
|
||||||
},
|
},
|
||||||
|
color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$isDrawing.set(false);
|
$isDrawing.set(false);
|
||||||
@ -127,12 +150,14 @@ export const setStageEventHandlers = ({
|
|||||||
}
|
}
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
const pos = syncCursorPos(stage, $lastCursorPos);
|
const pos = syncCursorPos(stage, $lastCursorPos);
|
||||||
const selectedLayerId = $selectedLayerId.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
const selectedLayerType = $selectedLayerType.get();
|
|
||||||
|
|
||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
|
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
|
||||||
@ -146,10 +171,21 @@ export const setStageEventHandlers = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$lastAddedPoint.set({ x: pos.x, y: pos.y });
|
$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 {
|
} else {
|
||||||
|
if (tool === 'brush') {
|
||||||
// Start a new line
|
// Start a new line
|
||||||
onRGLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool });
|
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);
|
$isDrawing.set(true);
|
||||||
}
|
}
|
||||||
@ -164,28 +200,36 @@ export const setStageEventHandlers = ({
|
|||||||
$isDrawing.set(false);
|
$isDrawing.set(false);
|
||||||
$lastCursorPos.set(null);
|
$lastCursorPos.set(null);
|
||||||
$lastMouseDownPos.set(null);
|
$lastMouseDownPos.set(null);
|
||||||
const selectedLayerId = $selectedLayerId.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
const selectedLayerType = $selectedLayerType.get();
|
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
|
|
||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
|
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) => {
|
stage.on('wheel', (e) => {
|
||||||
e.evt.preventDefault();
|
e.evt.preventDefault();
|
||||||
const selectedLayerType = $selectedLayerType.get();
|
|
||||||
const tool = $tool.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invert the delta if the property is set to true
|
// Invert the delta if the property is set to true
|
||||||
let delta = e.evt.deltaY;
|
let delta = e.evt.deltaY;
|
||||||
if ($shouldInvertBrushSizeScrollDirection.get()) {
|
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 LAYER_BBOX_NAME = 'layer.bbox';
|
||||||
export const COMPOSITING_RECT_NAME = 'compositing-rect';
|
export const COMPOSITING_RECT_NAME = 'compositing-rect';
|
||||||
export const RASTER_LAYER_NAME = 'raster_layer';
|
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
|
// Getters for non-singleton layer and object IDs
|
||||||
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
|
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
|
||||||
export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`;
|
export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`;
|
||||||
export const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
export const getBrushLineId = (layerId: string, lineId: string) => `${layerId}.brush_line_${lineId}`;
|
||||||
export const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
|
export const getEraserLineId = (layerId: string, lineId: string) => `${layerId}.eraser_line_${lineId}`;
|
||||||
export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
|
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 getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
||||||
export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
|
export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
|
||||||
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
||||||
|
@ -10,11 +10,13 @@ import {
|
|||||||
getCALayerImageId,
|
getCALayerImageId,
|
||||||
getIILayerImageId,
|
getIILayerImageId,
|
||||||
getLayerBboxId,
|
getLayerBboxId,
|
||||||
getRGLayerObjectGroupId,
|
getObjectGroupId,
|
||||||
INITIAL_IMAGE_LAYER_IMAGE_NAME,
|
INITIAL_IMAGE_LAYER_IMAGE_NAME,
|
||||||
INITIAL_IMAGE_LAYER_NAME,
|
INITIAL_IMAGE_LAYER_NAME,
|
||||||
LAYER_BBOX_NAME,
|
LAYER_BBOX_NAME,
|
||||||
NO_LAYERS_MESSAGE_LAYER_ID,
|
NO_LAYERS_MESSAGE_LAYER_ID,
|
||||||
|
RASTER_LAYER_NAME,
|
||||||
|
RASTER_LAYER_OBJECT_GROUP_NAME,
|
||||||
RG_LAYER_LINE_NAME,
|
RG_LAYER_LINE_NAME,
|
||||||
RG_LAYER_NAME,
|
RG_LAYER_NAME,
|
||||||
RG_LAYER_OBJECT_GROUP_NAME,
|
RG_LAYER_OBJECT_GROUP_NAME,
|
||||||
@ -30,6 +32,7 @@ import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/control
|
|||||||
import {
|
import {
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
isInitialImageLayer,
|
isInitialImageLayer,
|
||||||
|
isRasterLayer,
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
isRenderableLayer,
|
isRenderableLayer,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
@ -39,15 +42,17 @@ import type {
|
|||||||
EraserLine,
|
EraserLine,
|
||||||
InitialImageLayer,
|
InitialImageLayer,
|
||||||
Layer,
|
Layer,
|
||||||
|
RasterLayer,
|
||||||
RectShape,
|
RectShape,
|
||||||
RegionalGuidanceLayer,
|
RegionalGuidanceLayer,
|
||||||
|
RgbaColor,
|
||||||
Tool,
|
Tool,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
|
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import type { RgbColor } from 'react-colorful';
|
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@ -59,21 +64,6 @@ import {
|
|||||||
TRANSPARENCY_CHECKER_PATTERN,
|
TRANSPARENCY_CHECKER_PATTERN,
|
||||||
} from './constants';
|
} 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.
|
* Creates the singleton tool preview layer and all its objects.
|
||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
@ -130,7 +120,7 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
|
|||||||
const renderToolPreview = (
|
const renderToolPreview = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
color: RgbColor | null,
|
brushColor: RgbaColor,
|
||||||
selectedLayerType: Layer['type'] | null,
|
selectedLayerType: Layer['type'] | null,
|
||||||
globalMaskLayerOpacity: number,
|
globalMaskLayerOpacity: number,
|
||||||
cursorPos: Vector2d | null,
|
cursorPos: Vector2d | null,
|
||||||
@ -142,7 +132,7 @@ const renderToolPreview = (
|
|||||||
if (layerCount === 0) {
|
if (layerCount === 0) {
|
||||||
// We have no layers, so we should not render any tool
|
// We have no layers, so we should not render any tool
|
||||||
stage.container().style.cursor = 'default';
|
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
|
// Non-mask-guidance layers don't have tools
|
||||||
stage.container().style.cursor = 'not-allowed';
|
stage.container().style.cursor = 'not-allowed';
|
||||||
} else if (tool === 'move') {
|
} else if (tool === 'move') {
|
||||||
@ -173,14 +163,14 @@ const renderToolPreview = (
|
|||||||
assert(rectPreview, 'Rect preview not found');
|
assert(rectPreview, 'Rect preview not found');
|
||||||
|
|
||||||
// No need to render the brush preview if the cursor position or color is missing
|
// 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
|
// Update the fill circle
|
||||||
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`);
|
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`);
|
||||||
brushPreviewFill?.setAttrs({
|
brushPreviewFill?.setAttrs({
|
||||||
x: cursorPos.x,
|
x: cursorPos.x,
|
||||||
y: cursorPos.y,
|
y: cursorPos.y,
|
||||||
radius: brushSize / 2,
|
radius: brushSize / 2,
|
||||||
fill: rgbaColorToString({ ...color, a: globalMaskLayerOpacity }),
|
fill: rgbaColorToString(brushColor),
|
||||||
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
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)
|
// The object group holds all of the layer's objects (e.g. lines and rects)
|
||||||
const konvaObjectGroup = new Konva.Group({
|
const konvaObjectGroup = new Konva.Group({
|
||||||
id: getRGLayerObjectGroupId(layerState.id, uuidv4()),
|
id: getObjectGroupId(layerState.id, uuidv4()),
|
||||||
name: RG_LAYER_OBJECT_GROUP_NAME,
|
name: RG_LAYER_OBJECT_GROUP_NAME,
|
||||||
listening: false,
|
listening: false,
|
||||||
});
|
});
|
||||||
@ -273,13 +263,14 @@ const createRGLayer = (
|
|||||||
|
|
||||||
return konvaLayer;
|
return konvaLayer;
|
||||||
};
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a konva vector mask brush line from a vector mask line.
|
* Creates a konva line for a brush line.
|
||||||
* @param brushLine The vector mask line state
|
* @param brushLine The brush line state
|
||||||
* @param layerObjectGroup The konva layer's object group to add the line to
|
* @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({
|
const konvaLine = new Konva.Line({
|
||||||
id: brushLine.id,
|
id: brushLine.id,
|
||||||
key: brushLine.id,
|
key: brushLine.id,
|
||||||
@ -291,17 +282,18 @@ const createVectorMaskBrushLine = (brushLine: BrushLine, layerObjectGroup: Konva
|
|||||||
shadowForStrokeEnabled: false,
|
shadowForStrokeEnabled: false,
|
||||||
globalCompositeOperation: 'source-over',
|
globalCompositeOperation: 'source-over',
|
||||||
listening: false,
|
listening: false,
|
||||||
|
stroke: rgbaColorToString(brushLine.color),
|
||||||
});
|
});
|
||||||
layerObjectGroup.add(konvaLine);
|
layerObjectGroup.add(konvaLine);
|
||||||
return konvaLine;
|
return konvaLine;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a konva vector mask eraser line from a vector mask line.
|
* Creates a konva line for a eraser line.
|
||||||
* @param eraserLine The vector mask line state
|
* @param eraserLine The eraser line state
|
||||||
* @param layerObjectGroup The konva layer's object group to add the line to
|
* @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({
|
const konvaLine = new Konva.Line({
|
||||||
id: eraserLine.id,
|
id: eraserLine.id,
|
||||||
key: eraserLine.id,
|
key: eraserLine.id,
|
||||||
@ -313,42 +305,35 @@ const createVectorMaskEraserLine = (eraserLine: EraserLine, layerObjectGroup: Ko
|
|||||||
shadowForStrokeEnabled: false,
|
shadowForStrokeEnabled: false,
|
||||||
globalCompositeOperation: 'destination-out',
|
globalCompositeOperation: 'destination-out',
|
||||||
listening: false,
|
listening: false,
|
||||||
|
stroke: rgbaColorToString(DEFAULT_RGBA_COLOR),
|
||||||
});
|
});
|
||||||
layerObjectGroup.add(konvaLine);
|
layerObjectGroup.add(konvaLine);
|
||||||
return 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.
|
* Creates a konva rect for a rect shape.
|
||||||
* @param vectorMaskRect The vector mask rect state
|
* @param rectShape The rect shape state
|
||||||
* @param layerObjectGroup The konva layer's object group to add the line to
|
* @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({
|
const konvaRect = new Konva.Rect({
|
||||||
id: vectorMaskRect.id,
|
id: rectShape.id,
|
||||||
key: vectorMaskRect.id,
|
key: rectShape.id,
|
||||||
name: RG_LAYER_RECT_NAME,
|
name: RG_LAYER_RECT_NAME,
|
||||||
x: vectorMaskRect.x,
|
x: rectShape.x,
|
||||||
y: vectorMaskRect.y,
|
y: rectShape.y,
|
||||||
width: vectorMaskRect.width,
|
width: rectShape.width,
|
||||||
height: vectorMaskRect.height,
|
height: rectShape.height,
|
||||||
listening: false,
|
listening: false,
|
||||||
|
fill: rgbaColorToString(rectShape.color),
|
||||||
});
|
});
|
||||||
layerObjectGroup.add(konvaRect);
|
layerObjectGroup.add(konvaRect);
|
||||||
return konvaRect;
|
return konvaRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the "compositing rect" for a layer.
|
* Creates the "compositing rect" for a regional guidance layer.
|
||||||
* @param konvaLayer The konva layer
|
* @param konvaLayer The konva layer
|
||||||
*/
|
*/
|
||||||
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
|
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 stage The konva stage
|
||||||
* @param layerState The regional guidance layer state
|
* @param layerState The regional guidance layer state
|
||||||
* @param globalMaskLayerOpacity The global mask layer opacity
|
* @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.
|
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
|
||||||
let groupNeedsCache = false;
|
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
|
// Destroy any objects that are no longer in the redux state
|
||||||
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
|
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
|
||||||
if (!objectIds.includes(objectNode.id())) {
|
if (!objectIds.includes(objectNode.id())) {
|
||||||
@ -400,29 +385,41 @@ const renderRGLayer = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const maskObject of layerState.maskObjects) {
|
for (const obj of layerState.objects) {
|
||||||
if (maskObject.type === 'brush_line' || maskObject.type === 'eraser_line') {
|
if (obj.type === 'brush_line') {
|
||||||
const vectorMaskLine =
|
const konvaBrushLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup);
|
||||||
stage.findOne<Konva.Line>(`#${maskObject.id}`) ?? createVectorMaskLine(maskObject, konvaObjectGroup);
|
|
||||||
|
|
||||||
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
// 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.
|
// array, so checking the length is sufficient to determine if we need to re-cache.
|
||||||
if (vectorMaskLine.points().length !== maskObject.points.length) {
|
if (konvaBrushLine.points().length !== obj.points.length) {
|
||||||
vectorMaskLine.points(maskObject.points);
|
konvaBrushLine.points(obj.points);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
// Only update the color if it has changed.
|
// Only update the color if it has changed.
|
||||||
if (vectorMaskLine.stroke() !== rgbColor) {
|
if (konvaBrushLine.stroke() !== rgbColor) {
|
||||||
vectorMaskLine.stroke(rgbColor);
|
konvaBrushLine.stroke(rgbColor);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
} else if (maskObject.type === 'rect_shape') {
|
} else if (obj.type === 'eraser_line') {
|
||||||
const konvaObject =
|
const konvaEraserLine = stage.findOne<Konva.Line>(`#${obj.id}`) ?? createEraserLine(obj, konvaObjectGroup);
|
||||||
stage.findOne<Konva.Rect>(`#${maskObject.id}`) ?? createVectorMaskRect(maskObject, 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.
|
// Only update the color if it has changed.
|
||||||
if (konvaObject.fill() !== rgbColor) {
|
if (konvaRectShape.fill() !== rgbColor) {
|
||||||
konvaObject.fill(rgbColor);
|
konvaRectShape.fill(rgbColor);
|
||||||
groupNeedsCache = true;
|
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.
|
* Creates an initial image konva layer.
|
||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
@ -805,6 +922,9 @@ const renderLayers = (
|
|||||||
if (isInitialImageLayer(layer)) {
|
if (isInitialImageLayer(layer)) {
|
||||||
renderIILayer(stage, layer, getImageDTO);
|
renderIILayer(stage, layer, getImageDTO);
|
||||||
}
|
}
|
||||||
|
if (isRasterLayer(layer)) {
|
||||||
|
renderRasterLayer(stage, layer, tool, onLayerPosChanged);
|
||||||
|
}
|
||||||
// IP Adapter layers are not rendered
|
// IP Adapter layers are not rendered
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -886,7 +1006,7 @@ const updateBboxes = (
|
|||||||
const visible = bboxRect.visible();
|
const visible = bboxRect.visible();
|
||||||
bboxRect.visible(false);
|
bboxRect.visible(false);
|
||||||
|
|
||||||
if (rgLayer.maskObjects.length === 0) {
|
if (rgLayer.objects.length === 0) {
|
||||||
// No objects - no bbox to calculate
|
// No objects - no bbox to calculate
|
||||||
onBboxChanged(rgLayer.id, null);
|
onBboxChanged(rgLayer.id, null);
|
||||||
} else {
|
} else {
|
||||||
@ -1041,3 +1161,23 @@ export const debouncedRenderers = {
|
|||||||
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
||||||
updateBboxes: debounce(updateBboxes, 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 { deepClone } from 'common/util/deepClone';
|
||||||
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import {
|
import {
|
||||||
|
getBrushLineId,
|
||||||
getCALayerId,
|
getCALayerId,
|
||||||
|
getEraserLineId,
|
||||||
getIPALayerId,
|
getIPALayerId,
|
||||||
getRasterLayerId,
|
getRasterLayerId,
|
||||||
|
getRectId,
|
||||||
getRGLayerId,
|
getRGLayerId,
|
||||||
getRGLayerLineId,
|
|
||||||
getRGLayerRectId,
|
|
||||||
INITIAL_IMAGE_LAYER_ID,
|
INITIAL_IMAGE_LAYER_ID,
|
||||||
} from 'features/controlLayers/konva/naming';
|
} from 'features/controlLayers/konva/naming';
|
||||||
import type {
|
import type {
|
||||||
@ -45,20 +46,24 @@ import { assert } from 'tsafe';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AddLineArg,
|
AddBrushLineArg,
|
||||||
|
AddEraserLineArg,
|
||||||
AddPointToLineArg,
|
AddPointToLineArg,
|
||||||
AddRectArg,
|
AddRectShapeArg,
|
||||||
BrushLine,
|
BrushLine,
|
||||||
ControlAdapterLayer,
|
ControlAdapterLayer,
|
||||||
ControlLayersState,
|
ControlLayersState,
|
||||||
DrawingTool,
|
EllipseShape,
|
||||||
EraserLine,
|
EraserLine,
|
||||||
|
ImageObject,
|
||||||
InitialImageLayer,
|
InitialImageLayer,
|
||||||
IPAdapterLayer,
|
IPAdapterLayer,
|
||||||
Layer,
|
Layer,
|
||||||
|
PolygonShape,
|
||||||
RasterLayer,
|
RasterLayer,
|
||||||
RectShape,
|
RectShape,
|
||||||
RegionalGuidanceLayer,
|
RegionalGuidanceLayer,
|
||||||
|
RgbaColor,
|
||||||
Tool,
|
Tool,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { DEFAULT_RGBA_COLOR } from './types';
|
import { DEFAULT_RGBA_COLOR } from './types';
|
||||||
@ -67,6 +72,7 @@ export const initialControlLayersState: ControlLayersState = {
|
|||||||
_version: 3,
|
_version: 3,
|
||||||
selectedLayerId: null,
|
selectedLayerId: null,
|
||||||
brushSize: 100,
|
brushSize: 100,
|
||||||
|
brushColor: DEFAULT_RGBA_COLOR,
|
||||||
layers: [],
|
layers: [],
|
||||||
globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity
|
globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity
|
||||||
positivePrompt: '',
|
positivePrompt: '',
|
||||||
@ -81,8 +87,9 @@ export const initialControlLayersState: ControlLayersState = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLine = (obj: BrushLine | EraserLine | RectShape): obj is BrushLine | EraserLine =>
|
const isLine = (
|
||||||
obj.type === 'brush_line' || obj.type === 'eraser_line';
|
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 =>
|
export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer =>
|
||||||
layer?.type === 'regional_guidance_layer';
|
layer?.type === 'regional_guidance_layer';
|
||||||
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer =>
|
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer =>
|
||||||
@ -131,6 +138,14 @@ const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): Regio
|
|||||||
assert(isRegionalGuidanceLayer(layer));
|
assert(isRegionalGuidanceLayer(layer));
|
||||||
return 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 = (
|
export const selectRGLayerIPAdapterOrThrow = (
|
||||||
state: ControlLayersState,
|
state: ControlLayersState,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
@ -187,7 +202,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
layer.bboxNeedsUpdate = false;
|
layer.bboxNeedsUpdate = false;
|
||||||
if (bbox === null && layer.type === 'regional_guidance_layer') {
|
if (bbox === null && layer.type === 'regional_guidance_layer') {
|
||||||
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
||||||
layer.maskObjects = [];
|
layer.objects = [];
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,7 +211,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = state.layers.find((l) => l.id === action.payload);
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
// TODO(psyche): Should other layer types also have reset functionality?
|
// TODO(psyche): Should other layer types also have reset functionality?
|
||||||
if (isRegionalGuidanceLayer(layer)) {
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
layer.maskObjects = [];
|
layer.objects = [];
|
||||||
layer.bbox = null;
|
layer.bbox = null;
|
||||||
layer.isEnabled = true;
|
layer.isEnabled = true;
|
||||||
layer.bboxNeedsUpdate = false;
|
layer.bboxNeedsUpdate = false;
|
||||||
@ -455,7 +470,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
bbox: null,
|
bbox: null,
|
||||||
bboxNeedsUpdate: false,
|
bboxNeedsUpdate: false,
|
||||||
maskObjects: [],
|
objects: [],
|
||||||
previewColor: getVectorMaskPreviewColor(state),
|
previewColor: getVectorMaskPreviewColor(state),
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@ -490,81 +505,102 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectRGLayerOrThrow(state, layerId);
|
||||||
layer.previewColor = color;
|
layer.previewColor = color;
|
||||||
},
|
},
|
||||||
rgLayerLineAdded: {
|
brushLineAdded: {
|
||||||
reducer: (
|
reducer: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<
|
||||||
layerId: string;
|
AddBrushLineArg & {
|
||||||
points: [number, number, number, number];
|
|
||||||
tool: DrawingTool;
|
|
||||||
lineUuid: string;
|
lineUuid: string;
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
) => {
|
) => {
|
||||||
const { layerId, points, tool, lineUuid } = action.payload;
|
const { layerId, points, lineUuid, color } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
|
||||||
const lineId = getRGLayerLineId(layer.id, lineUuid);
|
layer.objects.push({
|
||||||
if (tool === 'brush') {
|
id: getBrushLineId(layer.id, lineUuid),
|
||||||
layer.maskObjects.push({
|
|
||||||
id: lineId,
|
|
||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
// Points must be offset by the layer's x and y coordinates
|
// Points must be offset by the layer's x and y coordinates
|
||||||
// TODO: Handle this in the event listener?
|
// TODO: Handle this in the event listener?
|
||||||
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
||||||
strokeWidth: state.brushSize,
|
strokeWidth: state.brushSize,
|
||||||
color: DEFAULT_RGBA_COLOR,
|
color,
|
||||||
});
|
});
|
||||||
} else {
|
layer.bboxNeedsUpdate = true;
|
||||||
layer.maskObjects.push({
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
id: lineId,
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: AddBrushLineArg) => ({
|
||||||
|
payload: { ...payload, lineUuid: uuidv4() },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
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',
|
type: 'eraser_line',
|
||||||
// Points must be offset by the layer's x and y coordinates
|
// Points must be offset by the layer's x and y coordinates
|
||||||
// TODO: Handle this in the event listener?
|
// TODO: Handle this in the event listener?
|
||||||
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
||||||
strokeWidth: state.brushSize,
|
strokeWidth: state.brushSize,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
prepare: (payload: AddLineArg) => ({
|
prepare: (payload: AddEraserLineArg) => ({
|
||||||
payload: { ...payload, lineUuid: uuidv4() },
|
payload: { ...payload, lineUuid: uuidv4() },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
rgLayerPointsAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
|
linePointsAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
|
||||||
const { layerId, point } = action.payload;
|
const { layerId, point } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
|
||||||
const lastLine = layer.maskObjects.findLast(isLine);
|
const lastLine = layer.objects.findLast(isLine);
|
||||||
if (!lastLine) {
|
if (!lastLine || !isLine(lastLine)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Points must be offset by the layer's x and y coordinates
|
// Points must be offset by the layer's x and y coordinates
|
||||||
// TODO: Handle this in the event listener
|
// TODO: Handle this in the event listener
|
||||||
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
|
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rgLayerRectAdded: {
|
rectAdded: {
|
||||||
reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => {
|
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectUuid: string }>) => {
|
||||||
const { layerId, rect, rectUuid } = action.payload;
|
const { layerId, rect, rectUuid, color } = action.payload;
|
||||||
if (rect.height === 0 || rect.width === 0) {
|
if (rect.height === 0 || rect.width === 0) {
|
||||||
// Ignore zero-area rectangles
|
// Ignore zero-area rectangles
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectRGOrRasterLayerOrThrow(state, layerId);
|
||||||
const id = getRGLayerRectId(layer.id, rectUuid);
|
const id = getRectId(layer.id, rectUuid);
|
||||||
layer.maskObjects.push({
|
layer.objects.push({
|
||||||
type: 'rect_shape',
|
type: 'rect_shape',
|
||||||
id,
|
id,
|
||||||
x: rect.x - layer.x,
|
x: rect.x - layer.x,
|
||||||
y: rect.y - layer.y,
|
y: rect.y - layer.y,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
color: DEFAULT_RGBA_COLOR,
|
color,
|
||||||
});
|
});
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
layer.uploadedMaskImage = null;
|
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 }>) => {
|
rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
@ -776,6 +812,9 @@ export const controlLayersSlice = createSlice({
|
|||||||
brushSizeChanged: (state, action: PayloadAction<number>) => {
|
brushSizeChanged: (state, action: PayloadAction<number>) => {
|
||||||
state.brushSize = Math.round(action.payload);
|
state.brushSize = Math.round(action.payload);
|
||||||
},
|
},
|
||||||
|
brushColorChanged: (state, action: PayloadAction<RgbaColor>) => {
|
||||||
|
state.brushColor = action.payload;
|
||||||
|
},
|
||||||
globalMaskLayerOpacityChanged: (state, action: PayloadAction<number>) => {
|
globalMaskLayerOpacityChanged: (state, action: PayloadAction<number>) => {
|
||||||
state.globalMaskLayerOpacity = action.payload;
|
state.globalMaskLayerOpacity = action.payload;
|
||||||
},
|
},
|
||||||
@ -892,9 +931,10 @@ export const {
|
|||||||
rgLayerPositivePromptChanged,
|
rgLayerPositivePromptChanged,
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
rgLayerPreviewColorChanged,
|
rgLayerPreviewColorChanged,
|
||||||
rgLayerLineAdded,
|
brushLineAdded,
|
||||||
rgLayerPointsAdded,
|
eraserLineAdded,
|
||||||
rgLayerRectAdded,
|
linePointsAdded,
|
||||||
|
rectAdded,
|
||||||
rgLayerMaskImageUploaded,
|
rgLayerMaskImageUploaded,
|
||||||
rgLayerAutoNegativeChanged,
|
rgLayerAutoNegativeChanged,
|
||||||
rgLayerIPAdapterAdded,
|
rgLayerIPAdapterAdded,
|
||||||
@ -924,6 +964,7 @@ export const {
|
|||||||
heightChanged,
|
heightChanged,
|
||||||
aspectRatioChanged,
|
aspectRatioChanged,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
|
brushColorChanged,
|
||||||
globalMaskLayerOpacityChanged,
|
globalMaskLayerOpacityChanged,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
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
|
// 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...
|
// TODO(psyche): This is a hack, figure out another way to handle this...
|
||||||
export const $brushSize = atom<number>(0);
|
export const $brushSize = atom<number>(0);
|
||||||
|
export const $brushColor = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
|
||||||
export const $brushSpacingPx = atom<number>(0);
|
export const $brushSpacingPx = atom<number>(0);
|
||||||
export const $selectedLayerId = atom<string | null>(null);
|
export const $selectedLayer = atom<Layer | null>(null);
|
||||||
export const $selectedLayerType = atom<Layer['type'] | null>(null);
|
|
||||||
export const $shouldInvertBrushSizeScrollDirection = atom(false);
|
export const $shouldInvertBrushSizeScrollDirection = atom(false);
|
||||||
|
|
||||||
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
|
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.
|
// 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
|
// 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.
|
// 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;
|
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) {
|
if (history.group === LINE_1 || history.group === LINE_2) {
|
||||||
return history.group;
|
return history.group;
|
||||||
}
|
}
|
||||||
@ -1012,15 +1053,6 @@ export const controlLayersUndoableConfig: UndoableOptions<ControlLayersState, Un
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
filter: (action, _state, _history) => {
|
filter: (action, _state, _history) => {
|
||||||
// Ignore all actions from other slices
|
|
||||||
if (!action.type.startsWith(controlLayersSlice.name)) {
|
|
||||||
return false;
|
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;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -55,7 +55,7 @@ const zRgbColor = z.object({
|
|||||||
const zRgbaColor = zRgbColor.extend({
|
const zRgbaColor = zRgbColor.extend({
|
||||||
a: z.number().min(0).max(1),
|
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 };
|
export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };
|
||||||
|
|
||||||
const zOpacity = z.number().gte(0).lte(1);
|
const zOpacity = z.number().gte(0).lte(1);
|
||||||
@ -193,7 +193,7 @@ const zMaskObject = z
|
|||||||
})
|
})
|
||||||
.pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape]));
|
.pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape]));
|
||||||
|
|
||||||
const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
|
const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({
|
||||||
type: z.literal('regional_guidance_layer'),
|
type: z.literal('regional_guidance_layer'),
|
||||||
maskObjects: z.array(zMaskObject),
|
maskObjects: z.array(zMaskObject),
|
||||||
positivePrompt: zParameterPositivePrompt.nullable(),
|
positivePrompt: zParameterPositivePrompt.nullable(),
|
||||||
@ -203,7 +203,28 @@ const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
|
|||||||
autoNegative: zAutoNegative,
|
autoNegative: zAutoNegative,
|
||||||
uploadedMaskImage: zImageWithDims.nullable(),
|
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({
|
const zInitialImageLayer = zRenderableLayerBase.extend({
|
||||||
type: z.literal('initial_image_layer'),
|
type: z.literal('initial_image_layer'),
|
||||||
@ -227,6 +248,7 @@ export type ControlLayersState = {
|
|||||||
selectedLayerId: string | null;
|
selectedLayerId: string | null;
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
|
brushColor: RgbaColor;
|
||||||
globalMaskLayerOpacity: number;
|
globalMaskLayerOpacity: number;
|
||||||
positivePrompt: ParameterPositivePrompt;
|
positivePrompt: ParameterPositivePrompt;
|
||||||
negativePrompt: ParameterNegativePrompt;
|
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 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