feat(ui): organize layer naming

prep for non-rp layer types
This commit is contained in:
psychedelicious 2024-04-19 15:18:26 +10:00 committed by Kent Keirsey
parent f3b4cecf2e
commit 642a0de3dd
14 changed files with 340 additions and 312 deletions

View File

@ -11,6 +11,7 @@ import {
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
PROMPT_REGION_POSITIVE_COND_PREFIX, PROMPT_REGION_POSITIVE_COND_PREFIX,
} from 'features/nodes/util/graph/constants'; } from 'features/nodes/util/graph/constants';
import { isRegionalPromptLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs'; import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs';
import { size } from 'lodash-es'; import { size } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
@ -22,7 +23,7 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
// TODO: Handle non-SDXL // TODO: Handle non-SDXL
// const isSDXL = state.generation.model?.base === 'sdxl'; // const isSDXL = state.generation.model?.base === 'sdxl';
const layers = state.regionalPrompts.present.layers const layers = state.regionalPrompts.present.layers
.filter((l) => l.kind === 'promptRegionLayer') // We only want the prompt region layers .filter(isRegionalPromptLayer) // We only want the prompt region layers
.filter((l) => l.isVisible) // Only visible layers are rendered on the canvas .filter((l) => l.isVisible) // Only visible layers are rendered on the canvas
.filter((l) => l.negativePrompt || l.positivePrompt); // Only layers with prompts get added to the graph .filter((l) => l.negativePrompt || l.positivePrompt); // Only layers with prompts get added to the graph

View File

@ -8,7 +8,7 @@ export const AddLayerButton = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(layerAdded('promptRegionLayer')); dispatch(layerAdded('regionalPromptLayer'));
}, [dispatch]); }, [dispatch]);
return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>; return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>;

View File

@ -4,7 +4,8 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import { isParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import { import {
layerAutoNegativeChanged, isRegionalPromptLayer,
rpLayerAutoNegativeChanged,
selectRegionalPromptsSlice, selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
@ -25,7 +26,7 @@ const useAutoNegative = (layerId: string) => {
() => () =>
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => { createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId); const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(isRegionalPromptLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return layer.autoNegative; return layer.autoNegative;
}), }),
[layerId] [layerId]
@ -44,7 +45,7 @@ const AutoNegativeCombobox = ({ layerId }: Props) => {
if (!isParameterAutoNegative(v?.value)) { if (!isParameterAutoNegative(v?.value)) {
return; return;
} }
dispatch(layerAutoNegativeChanged({ layerId, autoNegative: v.value })); dispatch(rpLayerAutoNegativeChanged({ layerId, autoNegative: v.value }));
}, },
[dispatch, layerId] [dispatch, layerId]
); );

View File

@ -3,7 +3,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker'; import RgbColorPicker from 'common/components/RgbColorPicker';
import { import {
promptRegionLayerColorChanged, isRegionalPromptLayer,
rpLayerColorChanged,
selectRegionalPromptsSlice, selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
@ -20,7 +21,7 @@ export const LayerColorPicker = memo(({ id }: Props) => {
() => () =>
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => { createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
const layer = regionalPrompts.present.layers.find((l) => l.id === id); const layer = regionalPrompts.present.layers.find((l) => l.id === id);
assert(layer); assert(isRegionalPromptLayer(layer), `Layer ${id} not found or not an RP layer`);
return layer.color; return layer.color;
}), }),
[id] [id]
@ -29,7 +30,7 @@ export const LayerColorPicker = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onColorChange = useCallback( const onColorChange = useCallback(
(color: RgbColor) => { (color: RgbColor) => {
dispatch(promptRegionLayerColorChanged({ layerId: id, color })); dispatch(rpLayerColorChanged({ layerId: id, color }));
}, },
[dispatch, id] [dispatch, id]
); );

View File

@ -7,8 +7,9 @@ import { LayerMenu } from 'features/regionalPrompts/components/LayerMenu';
import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle';
import { RegionalPromptsNegativePrompt } from 'features/regionalPrompts/components/RegionalPromptsNegativePrompt'; import { RegionalPromptsNegativePrompt } from 'features/regionalPrompts/components/RegionalPromptsNegativePrompt';
import { RegionalPromptsPositivePrompt } from 'features/regionalPrompts/components/RegionalPromptsPositivePrompt'; import { RegionalPromptsPositivePrompt } from 'features/regionalPrompts/components/RegionalPromptsPositivePrompt';
import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { isRegionalPromptLayer, rpLayerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { assert } from 'tsafe';
type Props = { type Props = {
id: string; id: string;
@ -18,15 +19,13 @@ export const LayerListItem = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selectedLayer = useAppSelector((s) => s.regionalPrompts.present.selectedLayer); const selectedLayer = useAppSelector((s) => s.regionalPrompts.present.selectedLayer);
const color = useAppSelector((s) => { const color = useAppSelector((s) => {
const color = s.regionalPrompts.present.layers.find((l) => l.id === id)?.color; const layer = s.regionalPrompts.present.layers.find((l) => l.id === id);
if (color) { assert(isRegionalPromptLayer(layer), `Layer ${id} not found or not an RP layer`);
return rgbaColorToString({ ...color, a: selectedLayer === id ? 1 : 0.35 }); return rgbaColorToString({ ...layer.color, a: selectedLayer === id ? 1 : 0.35 });
}
return 'base.700';
}); });
const onClickCapture = useCallback(() => { const onClickCapture = useCallback(() => {
// Must be capture so that the layer is selected before deleting/resetting/etc // Must be capture so that the layer is selected before deleting/resetting/etc
dispatch(layerSelected(id)); dispatch(rpLayerSelected(id));
}, [dispatch, id]); }, [dispatch, id]);
return ( return (
<Flex gap={2} onClickCapture={onClickCapture} bg={color} borderRadius="base" p="1px" ps={3}> <Flex gap={2} onClickCapture={onClickCapture} bg={color} borderRadius="base" p="1px" ps={3}>

View File

@ -7,7 +7,7 @@ import {
layerMovedForward, layerMovedForward,
layerMovedToBack, layerMovedToBack,
layerMovedToFront, layerMovedToFront,
layerReset, rpLayerReset,
selectRegionalPromptsSlice, selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
@ -55,7 +55,7 @@ export const LayerMenu = memo(({ id }: Props) => {
dispatch(layerMovedToBack(id)); dispatch(layerMovedToBack(id));
}, [dispatch, id]); }, [dispatch, id]);
const resetLayer = useCallback(() => { const resetLayer = useCallback(() => {
dispatch(layerReset(id)); dispatch(rpLayerReset(id));
}, [dispatch, id]); }, [dispatch, id]);
const deleteLayer = useCallback(() => { const deleteLayer = useCallback(() => {
dispatch(layerDeleted(id)); dispatch(layerDeleted(id));

View File

@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useLayerIsVisible } from 'features/regionalPrompts/hooks/layerStateHooks'; import { useLayerIsVisible } from 'features/regionalPrompts/hooks/layerStateHooks';
import { layerIsVisibleToggled } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { rpLayerIsVisibleToggled } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi'; import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi';
@ -13,7 +13,7 @@ export const LayerVisibilityToggle = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isVisible = useLayerIsVisible(id); const isVisible = useLayerIsVisible(id);
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(layerIsVisibleToggled(id)); dispatch(rpLayerIsVisibleToggled(id));
}, [dispatch, id]); }, [dispatch, id]);
return ( return (

View File

@ -5,7 +5,7 @@ import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover'; import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt'; import { usePrompt } from 'features/prompt/usePrompt';
import { useLayerNegativePrompt } from 'features/regionalPrompts/hooks/layerStateHooks'; import { useLayerNegativePrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
import { negativePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { rpLayerNegativePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import type { HotkeyCallback } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -22,7 +22,7 @@ export const RegionalPromptsNegativePrompt = memo((props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const _onChange = useCallback( const _onChange = useCallback(
(v: string) => { (v: string) => {
dispatch(negativePromptChanged({ layerId: props.layerId, prompt: v })); dispatch(rpLayerNegativePromptChanged({ layerId: props.layerId, prompt: v }));
}, },
[dispatch, props.layerId] [dispatch, props.layerId]
); );

View File

@ -5,7 +5,7 @@ import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover'; import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt'; import { usePrompt } from 'features/prompt/usePrompt';
import { useLayerPositivePrompt } from 'features/regionalPrompts/hooks/layerStateHooks'; import { useLayerPositivePrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
import { positivePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { rpLayerPositivePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useRef } from 'react'; import { memo, useCallback, useRef } from 'react';
import type { HotkeyCallback } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -22,7 +22,7 @@ export const RegionalPromptsPositivePrompt = memo((props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const _onChange = useCallback( const _onChange = useCallback(
(v: string) => { (v: string) => {
dispatch(positivePromptChanged({ layerId: props.layerId, prompt: v })); dispatch(rpLayerPositivePromptChanged({ layerId: props.layerId, prompt: v }));
}, },
[dispatch, props.layerId] [dispatch, props.layerId]
); );

View File

@ -6,8 +6,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks'; import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks';
import { import {
$cursorPosition, $cursorPosition,
layerBboxChanged, isRegionalPromptLayer,
layerTranslated, rpLayerBboxChanged,
rpLayerTranslated,
selectRegionalPromptsSlice, selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { renderBbox, renderBrushPreview, renderLayers } from 'features/regionalPrompts/util/renderers'; import { renderBbox, renderBrushPreview, renderLayers } from 'features/regionalPrompts/util/renderers';
@ -15,11 +16,17 @@ import Konva from 'konva';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import { useCallback, useLayoutEffect } from 'react'; import { useCallback, useLayoutEffect } from 'react';
import { assert } from 'tsafe';
const log = logger('regionalPrompts'); const log = logger('regionalPrompts');
const $stage = atom<Konva.Stage | null>(null); const $stage = atom<Konva.Stage | null>(null);
const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => { const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
return regionalPrompts.present.layers.find((l) => l.id === regionalPrompts.present.selectedLayer)?.color ?? null; const layer = regionalPrompts.present.layers.find((l) => l.id === regionalPrompts.present.selectedLayer);
if (!layer) {
return null;
}
assert(isRegionalPromptLayer(layer), `Layer ${regionalPrompts.present.selectedLayer} is not an RP layer`);
return layer.color;
}); });
const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElement | null) => { const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElement | null) => {
@ -34,14 +41,14 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
const onLayerPosChanged = useCallback( const onLayerPosChanged = useCallback(
(layerId: string, x: number, y: number) => { (layerId: string, x: number, y: number) => {
dispatch(layerTranslated({ layerId, x, y })); dispatch(rpLayerTranslated({ layerId, x, y }));
}, },
[dispatch] [dispatch]
); );
const onBboxChanged = useCallback( const onBboxChanged = useCallback(
(layerId: string, bbox: IRect) => { (layerId: string, bbox: IRect) => {
dispatch(layerBboxChanged({ layerId, bbox })); dispatch(rpLayerBboxChanged({ layerId, bbox }));
}, },
[dispatch] [dispatch]
); );

View File

@ -1,47 +1,47 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { isRegionalPromptLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
export const useLayerPositivePrompt = (layerId: string) => { export const useLayerPositivePrompt = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector( createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
selectRegionalPromptsSlice, const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
(regionalPrompts) => regionalPrompts.present.layers.find((l) => l.id === layerId)?.positivePrompt assert(isRegionalPromptLayer(layer), `Layer ${layerId} not found or not an RP layer`);
), return layer.positivePrompt;
}),
[layerId] [layerId]
); );
const prompt = useAppSelector(selectLayer); const prompt = useAppSelector(selectLayer);
assert(prompt !== undefined, `Layer ${layerId} doesn't exist!`);
return prompt; return prompt;
}; };
export const useLayerNegativePrompt = (layerId: string) => { export const useLayerNegativePrompt = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector( createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
selectRegionalPromptsSlice, const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
(regionalPrompts) => regionalPrompts.present.layers.find((l) => l.id === layerId)?.negativePrompt assert(isRegionalPromptLayer(layer), `Layer ${layerId} not found or not an RP layer`);
), return layer.negativePrompt;
}),
[layerId] [layerId]
); );
const prompt = useAppSelector(selectLayer); const prompt = useAppSelector(selectLayer);
assert(prompt !== undefined, `Layer ${layerId} doesn't exist!`);
return prompt; return prompt;
}; };
export const useLayerIsVisible = (layerId: string) => { export const useLayerIsVisible = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector( createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
selectRegionalPromptsSlice, const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
(regionalPrompts) => regionalPrompts.present.layers.find((l) => l.id === layerId)?.isVisible assert(isRegionalPromptLayer(layer), `Layer ${layerId} not found or not an RP layer`);
), return layer.isVisible;
}),
[layerId] [layerId]
); );
const isVisible = useAppSelector(selectLayer); const isVisible = useAppSelector(selectLayer);
assert(isVisible !== undefined, `Layer ${layerId} doesn't exist!`);
return isVisible; return isVisible;
}; };

View File

@ -5,8 +5,8 @@ import {
$cursorPosition, $cursorPosition,
$isMouseDown, $isMouseDown,
$isMouseOver, $isMouseOver,
lineAdded, rpLayerLineAdded,
pointsAdded, rpLayerPointsAdded,
} from 'features/regionalPrompts/store/regionalPromptsSlice'; } from 'features/regionalPrompts/store/regionalPromptsSlice';
import type Konva from 'konva'; import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node'; import type { KonvaEventObject } from 'konva/lib/Node';
@ -43,7 +43,7 @@ export const useMouseEvents = () => {
$isMouseDown.set(true); $isMouseDown.set(true);
const tool = getTool(); const tool = getTool();
if (tool === 'brush' || tool === 'eraser') { if (tool === 'brush' || tool === 'eraser') {
dispatch(lineAdded([pos.x, pos.y, pos.x, pos.y])); dispatch(rpLayerLineAdded([pos.x, pos.y, pos.x, pos.y]));
} }
}, },
[dispatch] [dispatch]
@ -75,7 +75,7 @@ export const useMouseEvents = () => {
} }
const tool = getTool(); const tool = getTool();
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) { if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
dispatch(pointsAdded([pos.x, pos.y])); dispatch(rpLayerPointsAdded([pos.x, pos.y]));
} }
}, },
[dispatch] [dispatch]
@ -111,7 +111,7 @@ export const useMouseEvents = () => {
$isMouseDown.set(true); $isMouseDown.set(true);
const tool = getTool(); const tool = getTool();
if (tool === 'brush' || tool === 'eraser') { if (tool === 'brush' || tool === 'eraser') {
dispatch(lineAdded([pos.x, pos.y, pos.x, pos.y])); dispatch(rpLayerLineAdded([pos.x, pos.y, pos.x, pos.y]));
} }
} }
}, },

View File

@ -44,14 +44,14 @@ type LayerObject = ImageObject | LineObject | FillRectObject;
type LayerBase = { type LayerBase = {
id: string; id: string;
};
export type RegionalPromptLayer = LayerBase & {
isVisible: boolean; isVisible: boolean;
x: number; x: number;
y: number; y: number;
bbox: IRect | null; bbox: IRect | null;
}; kind: 'regionalPromptLayer';
type PromptRegionLayer = LayerBase & {
kind: 'promptRegionLayer';
objects: LayerObject[]; objects: LayerObject[];
positivePrompt: string; positivePrompt: string;
negativePrompt: string; negativePrompt: string;
@ -59,13 +59,13 @@ type PromptRegionLayer = LayerBase & {
autoNegative: ParameterAutoNegative; autoNegative: ParameterAutoNegative;
}; };
export type Layer = PromptRegionLayer; export type Layer = RegionalPromptLayer;
type RegionalPromptsState = { type RegionalPromptsState = {
_version: 1; _version: 1;
tool: Tool; tool: Tool;
selectedLayer: string | null; selectedLayer: string | null;
layers: PromptRegionLayer[]; layers: Layer[];
brushSize: number; brushSize: number;
promptLayerOpacity: number; promptLayerOpacity: number;
}; };
@ -80,15 +80,19 @@ export const initialRegionalPromptsState: RegionalPromptsState = {
}; };
const isLine = (obj: LayerObject): obj is LineObject => obj.kind === 'line'; const isLine = (obj: LayerObject): obj is LineObject => obj.kind === 'line';
export const isRegionalPromptLayer = (layer?: Layer): layer is RegionalPromptLayer =>
layer?.kind === 'regionalPromptLayer';
export const regionalPromptsSlice = createSlice({ export const regionalPromptsSlice = createSlice({
name: 'regionalPrompts', name: 'regionalPrompts',
initialState: initialRegionalPromptsState, initialState: initialRegionalPromptsState,
reducers: { reducers: {
//#region Meta Layer
layerAdded: { layerAdded: {
reducer: (state, action: PayloadAction<Layer['kind'], string, { uuid: string; color: RgbColor }>) => { reducer: (state, action: PayloadAction<Layer['kind'], string, { uuid: string; color: RgbColor }>) => {
const layer: PromptRegionLayer = { if (action.payload === 'regionalPromptLayer') {
id: getLayerId(action.meta.uuid), const layer: RegionalPromptLayer = {
id: getRPLayerId(action.meta.uuid),
isVisible: true, isVisible: true,
bbox: null, bbox: null,
kind: action.payload, kind: action.payload,
@ -102,28 +106,11 @@ export const regionalPromptsSlice = createSlice({
}; };
state.layers.push(layer); state.layers.push(layer);
state.selectedLayer = layer.id; state.selectedLayer = layer.id;
return;
}
}, },
prepare: (payload: Layer['kind']) => ({ payload, meta: { uuid: uuidv4(), color: LayerColors.next() } }), prepare: (payload: Layer['kind']) => ({ payload, meta: { uuid: uuidv4(), color: LayerColors.next() } }),
}, },
layerSelected: (state, action: PayloadAction<string>) => {
state.selectedLayer = action.payload;
},
layerIsVisibleToggled: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload);
if (!layer) {
return;
}
layer.isVisible = !layer.isVisible;
},
layerReset: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload);
if (!layer) {
return;
}
layer.objects = [];
layer.bbox = null;
layer.isVisible = true;
},
layerDeleted: (state, action: PayloadAction<string>) => { layerDeleted: (state, action: PayloadAction<string>) => {
state.layers = state.layers.filter((l) => l.id !== action.payload); state.layers = state.layers.filter((l) => l.id !== action.payload);
state.selectedLayer = state.layers[0]?.id ?? null; state.selectedLayer = state.layers[0]?.id ?? null;
@ -146,58 +133,73 @@ export const regionalPromptsSlice = createSlice({
// Because the layers are in reverse order, moving to the back is equivalent to moving to the front // Because the layers are in reverse order, moving to the back is equivalent to moving to the front
moveToFront(state.layers, cb); moveToFront(state.layers, cb);
}, },
layerTranslated: (state, action: PayloadAction<{ layerId: string; x: number; y: number }>) => { //#endregion
//#region RP Layers
rpLayerSelected: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload);
if (isRegionalPromptLayer(layer)) {
state.selectedLayer = layer.id;
}
},
rpLayerIsVisibleToggled: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload);
if (isRegionalPromptLayer(layer)) {
layer.isVisible = !layer.isVisible;
}
},
rpLayerReset: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload);
if (isRegionalPromptLayer(layer)) {
layer.objects = [];
layer.bbox = null;
layer.isVisible = true;
}
},
rpLayerTranslated: (state, action: PayloadAction<{ layerId: string; x: number; y: number }>) => {
const { layerId, x, y } = action.payload; const { layerId, x, y } = action.payload;
const layer = state.layers.find((l) => l.id === layerId); const layer = state.layers.find((l) => l.id === layerId);
if (!layer) { if (isRegionalPromptLayer(layer)) {
return;
}
layer.x = x; layer.x = x;
layer.y = y; layer.y = y;
}
}, },
layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => { rpLayerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => {
const { layerId, bbox } = action.payload; const { layerId, bbox } = action.payload;
const layer = state.layers.find((l) => l.id === layerId); const layer = state.layers.find((l) => l.id === layerId);
if (!layer) { if (isRegionalPromptLayer(layer)) {
return;
}
layer.bbox = bbox; layer.bbox = bbox;
}
}, },
allLayersDeleted: (state) => { allLayersDeleted: (state) => {
state.layers = []; state.layers = [];
state.selectedLayer = null; state.selectedLayer = null;
}, },
positivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => { rpLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
const { layerId, prompt } = action.payload; const { layerId, prompt } = action.payload;
const layer = state.layers.find((l) => l.id === layerId); const layer = state.layers.find((l) => l.id === layerId);
if (!layer) { if (isRegionalPromptLayer(layer)) {
return;
}
layer.positivePrompt = prompt; layer.positivePrompt = prompt;
}
}, },
negativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => { rpLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
const { layerId, prompt } = action.payload; const { layerId, prompt } = action.payload;
const layer = state.layers.find((l) => l.id === layerId); const layer = state.layers.find((l) => l.id === layerId);
if (!layer) { if (isRegionalPromptLayer(layer)) {
return;
}
layer.negativePrompt = prompt; layer.negativePrompt = prompt;
}
}, },
promptRegionLayerColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { rpLayerColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
const { layerId, color } = action.payload; const { layerId, color } = action.payload;
const layer = state.layers.find((l) => l.id === layerId); const layer = state.layers.find((l) => l.id === layerId);
if (!layer || layer.kind !== 'promptRegionLayer') { if (isRegionalPromptLayer(layer)) {
return;
}
layer.color = color; layer.color = color;
}
}, },
lineAdded: { rpLayerLineAdded: {
reducer: (state, action: PayloadAction<[number, number, number, number], string, { uuid: string }>) => { reducer: (state, action: PayloadAction<[number, number, number, number], string, { uuid: string }>) => {
const layer = state.layers.find((l) => l.id === state.selectedLayer); const layer = state.layers.find((l) => l.id === state.selectedLayer);
if (!layer || layer.kind !== 'promptRegionLayer') { if (isRegionalPromptLayer(layer)) {
return; const lineId = getRPLayerLineId(layer.id, action.meta.uuid);
}
const lineId = getLayerLineId(layer.id, action.meta.uuid);
layer.objects.push({ layer.objects.push({
kind: 'line', kind: 'line',
tool: state.tool, tool: state.tool,
@ -210,20 +212,32 @@ export const regionalPromptsSlice = createSlice({
], ],
strokeWidth: state.brushSize, strokeWidth: state.brushSize,
}); });
}
}, },
prepare: (payload: [number, number, number, number]) => ({ payload, meta: { uuid: uuidv4() } }), prepare: (payload: [number, number, number, number]) => ({ payload, meta: { uuid: uuidv4() } }),
}, },
pointsAdded: (state, action: PayloadAction<[number, number]>) => { rpLayerPointsAdded: (state, action: PayloadAction<[number, number]>) => {
const layer = state.layers.find((l) => l.id === state.selectedLayer); const layer = state.layers.find((l) => l.id === state.selectedLayer);
if (!layer || layer.kind !== 'promptRegionLayer') { if (isRegionalPromptLayer(layer)) {
return;
}
const lastLine = layer.objects.findLast(isLine); const lastLine = layer.objects.findLast(isLine);
if (!lastLine) { if (!lastLine) {
return; return;
} }
lastLine.points.push(action.payload[0] - layer.x, action.payload[1] - layer.y); lastLine.points.push(action.payload[0] - layer.x, action.payload[1] - layer.y);
}
}, },
rpLayerAutoNegativeChanged: (
state,
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
) => {
const { layerId, autoNegative } = action.payload;
const layer = state.layers.find((l) => l.id === layerId);
if (isRegionalPromptLayer(layer)) {
layer.autoNegative = autoNegative;
}
},
//#endregion
//#region General
brushSizeChanged: (state, action: PayloadAction<number>) => { brushSizeChanged: (state, action: PayloadAction<number>) => {
state.brushSize = action.payload; state.brushSize = action.payload;
}, },
@ -233,17 +247,7 @@ export const regionalPromptsSlice = createSlice({
promptLayerOpacityChanged: (state, action: PayloadAction<number>) => { promptLayerOpacityChanged: (state, action: PayloadAction<number>) => {
state.promptLayerOpacity = action.payload; state.promptLayerOpacity = action.payload;
}, },
layerAutoNegativeChanged: ( //#endregion
state,
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
) => {
const { layerId, autoNegative } = action.payload;
const layer = state.layers.find((l) => l.id === layerId);
if (!layer || layer.kind !== 'promptRegionLayer') {
return;
}
layer.autoNegative = autoNegative;
},
}, },
}); });
@ -272,27 +276,28 @@ class LayerColors {
} }
export const { export const {
layerAdded,
layerSelected,
layerReset,
layerDeleted,
layerIsVisibleToggled,
positivePromptChanged,
negativePromptChanged,
lineAdded,
pointsAdded,
promptRegionLayerColorChanged,
brushSizeChanged,
layerMovedForward,
layerMovedToFront,
layerMovedBackward,
layerMovedToBack,
toolChanged,
layerTranslated,
layerBboxChanged,
promptLayerOpacityChanged,
allLayersDeleted, allLayersDeleted,
layerAutoNegativeChanged, brushSizeChanged,
layerAdded,
layerDeleted,
layerMovedBackward,
layerMovedForward,
layerMovedToBack,
layerMovedToFront,
promptLayerOpacityChanged,
toolChanged,
// Regional Prompt layer actions
rpLayerAutoNegativeChanged,
rpLayerBboxChanged,
rpLayerColorChanged,
rpLayerIsVisibleToggled,
rpLayerLineAdded,
rpLayerNegativePromptChanged,
rpLayerPointsAdded,
rpLayerPositivePromptChanged,
rpLayerReset,
rpLayerSelected,
rpLayerTranslated,
} = regionalPromptsSlice.actions; } = regionalPromptsSlice.actions;
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts; export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
@ -319,11 +324,11 @@ export const REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME = 'regionalPromptLayerObjec
export const REGIONAL_PROMPT_LAYER_BBOX_NAME = 'regionalPromptLayerBbox'; export const REGIONAL_PROMPT_LAYER_BBOX_NAME = 'regionalPromptLayerBbox';
// Getters for non-singleton layer and object IDs // Getters for non-singleton layer and object IDs
const getLayerId = (layerId: string) => `layer_${layerId}`; const getRPLayerId = (layerId: string) => `rp_layer_${layerId}`;
const getLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; const getRPLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
export const getLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getRPLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; export const getPRLayerBboxId = (layerId: string) => `${layerId}.bbox`;
export const getLayerTransparencyRectId = (layerId: string) => `${layerId}.transparency_rect`; export const getRPLayerTransparencyRectId = (layerId: string) => `${layerId}.transparency_rect`;
export const regionalPromptsPersistConfig: PersistConfig<RegionalPromptsState> = { export const regionalPromptsPersistConfig: PersistConfig<RegionalPromptsState> = {
name: regionalPromptsSlice.name, name: regionalPromptsSlice.name,
@ -339,11 +344,11 @@ export const clearHistoryRegionalPrompts = createAction(`${regionalPromptsSlice.
// These actions are _individually_ grouped together as single undoable actions // These actions are _individually_ grouped together as single undoable actions
const undoableGroupByMatcher = isAnyOf( const undoableGroupByMatcher = isAnyOf(
positivePromptChanged,
negativePromptChanged,
brushSizeChanged, brushSizeChanged,
layerTranslated, promptLayerOpacityChanged,
promptLayerOpacityChanged rpLayerPositivePromptChanged,
rpLayerNegativePromptChanged,
rpLayerTranslated
); );
const LINE_1 = 'LINE_1'; const LINE_1 = 'LINE_1';
@ -355,13 +360,13 @@ export const regionalPromptsUndoableConfig: UndoableOptions<RegionalPromptsState
redoType: redoRegionalPrompts.type, redoType: redoRegionalPrompts.type,
clearHistoryType: clearHistoryRegionalPrompts.type, clearHistoryType: clearHistoryRegionalPrompts.type,
groupBy: (action, state, history) => { groupBy: (action, state, history) => {
// Lines are started with `lineAdded` and may have any number of subsequent `pointsAdded` events. We can use a // Lines are started with `rpLayerLineAdded` and may have any number of subsequent `rpLayerPointsAdded` events.
// double-buffering-ish trick to group each logical line as a single undoable action, without grouping separate // We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping
// logical lines as a single undo action. // separate logical lines as a single undo action.
if (lineAdded.match(action)) { if (rpLayerLineAdded.match(action)) {
return history.group === LINE_1 ? LINE_2 : LINE_1; return history.group === LINE_1 ? LINE_2 : LINE_1;
} }
if (pointsAdded.match(action)) { if (rpLayerPointsAdded.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;
} }
@ -378,7 +383,7 @@ export const regionalPromptsUndoableConfig: UndoableOptions<RegionalPromptsState
} }
// This action is triggered on state changes, including when we undo. If we do not ignore this action, when we // 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. // undo, this action triggers and empties the future states array. Therefore, we must ignore this action.
if (layerBboxChanged.match(action)) { if (rpLayerBboxChanged.match(action)) {
return false; return false;
} }
// We don't want to record tool changes in the undo history // We don't want to record tool changes in the undo history

View File

@ -1,14 +1,14 @@
import { rgbColorToString } from 'features/canvas/util/colorToString'; import { rgbColorToString } from 'features/canvas/util/colorToString';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import type { Layer, Tool } from 'features/regionalPrompts/store/regionalPromptsSlice'; import type { Layer, RegionalPromptLayer, Tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { import {
BRUSH_PREVIEW_BORDER_INNER_ID, BRUSH_PREVIEW_BORDER_INNER_ID,
BRUSH_PREVIEW_BORDER_OUTER_ID, BRUSH_PREVIEW_BORDER_OUTER_ID,
BRUSH_PREVIEW_FILL_ID, BRUSH_PREVIEW_FILL_ID,
BRUSH_PREVIEW_LAYER_ID, BRUSH_PREVIEW_LAYER_ID,
getLayerBboxId, getPRLayerBboxId,
getLayerObjectGroupId, getRPLayerObjectGroupId,
getLayerTransparencyRectId, getRPLayerTransparencyRectId,
REGIONAL_PROMPT_LAYER_BBOX_NAME, REGIONAL_PROMPT_LAYER_BBOX_NAME,
REGIONAL_PROMPT_LAYER_LINE_NAME, REGIONAL_PROMPT_LAYER_LINE_NAME,
REGIONAL_PROMPT_LAYER_NAME, REGIONAL_PROMPT_LAYER_NAME,
@ -125,41 +125,21 @@ export const renderBrushPreview = (
}); });
}; };
/** const renderRPLayer = (
* Renders the layers on the stage.
* @param stage The konva stage to render on.
* @param reduxLayers Array of the layers from the redux store.
* @param selectedLayerId The selected layer id.
* @param layerOpacity The opacity of the layer.
* @param onLayerPosChanged Callback for when the layer's position changes. This is optional to allow for offscreen rendering.
* @returns
*/
export const renderLayers = (
stage: Konva.Stage, stage: Konva.Stage,
reduxLayers: Layer[], rpLayer: RegionalPromptLayer,
rpLayerIndex: number,
selectedLayerId: string | null, selectedLayerId: string | null,
layerOpacity: number,
tool: Tool, tool: Tool,
layerOpacity: number,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onLayerPosChanged?: (layerId: string, x: number, y: number) => void
) => { ) => {
const reduxLayerIds = reduxLayers.map(mapId); let konvaLayer = stage.findOne<Konva.Layer>(`#${rpLayer.id}`);
// Remove un-rendered layers
for (const konvaLayer of stage.find<Konva.Layer>(`.${REGIONAL_PROMPT_LAYER_NAME}`)) {
if (!reduxLayerIds.includes(konvaLayer.id())) {
konvaLayer.destroy();
}
}
for (let layerIndex = 0; layerIndex < reduxLayers.length; layerIndex++) {
const reduxLayer = reduxLayers[layerIndex];
assert(reduxLayer, `Layer at index ${layerIndex} is undefined`);
let konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
if (!konvaLayer) { if (!konvaLayer) {
// This layer hasn't been added to the konva state yet // This layer hasn't been added to the konva state yet
konvaLayer = new Konva.Layer({ konvaLayer = new Konva.Layer({
id: reduxLayer.id, id: rpLayer.id,
name: REGIONAL_PROMPT_LAYER_NAME, name: REGIONAL_PROMPT_LAYER_NAME,
draggable: true, draggable: true,
}); });
@ -167,7 +147,7 @@ export const renderLayers = (
// Create a `dragmove` listener for this layer // Create a `dragmove` listener for this layer
if (onLayerPosChanged) { if (onLayerPosChanged) {
konvaLayer.on('dragend', function (e) { konvaLayer.on('dragend', function (e) {
onLayerPosChanged(reduxLayer.id, e.target.x(), e.target.y()); onLayerPosChanged(rpLayer.id, e.target.x(), e.target.y());
}); });
} }
@ -191,7 +171,7 @@ export const renderLayers = (
// 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: getLayerObjectGroupId(reduxLayer.id, uuidv4()), id: getRPLayerObjectGroupId(rpLayer.id, uuidv4()),
name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME, name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
listening: false, listening: false,
}); });
@ -201,7 +181,7 @@ export const renderLayers = (
// The brush strokes group functions as a mask for this rect, which has the layer's fill and opacity. The brush // The brush strokes group functions as a mask for this rect, which has the layer's fill and opacity. The brush
// strokes' color doesn't matter - the only requirement is that they are not transparent. // strokes' color doesn't matter - the only requirement is that they are not transparent.
const transparencyRect = new Konva.Rect({ const transparencyRect = new Konva.Rect({
id: getLayerTransparencyRectId(reduxLayer.id), id: getRPLayerTransparencyRectId(rpLayer.id),
globalCompositeOperation: 'source-in', globalCompositeOperation: 'source-in',
listening: false, listening: false,
}); });
@ -215,29 +195,29 @@ export const renderLayers = (
// Update the layer's position and listening state (only the selected layer is listening) // Update the layer's position and listening state (only the selected layer is listening)
konvaLayer.setAttrs({ konvaLayer.setAttrs({
listening: reduxLayer.id === selectedLayerId && tool === 'move', listening: rpLayer.id === selectedLayerId && tool === 'move',
x: reduxLayer.x, x: rpLayer.x,
y: reduxLayer.y, y: rpLayer.y,
// There are reduxLayers.length layers, plus a brush preview layer rendered on top of them, so the zIndex works // There are rpLayers.length layers, plus a brush preview layer rendered on top of them, so the zIndex works
// out to be the layerIndex. If more layers are added, this may no longer be true. // out to be the layerIndex. If more layers are added, this may no longer be true.
zIndex: layerIndex, zIndex: rpLayerIndex,
}); });
const color = rgbColorToString(reduxLayer.color); const color = rgbColorToString(rpLayer.color);
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`); const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`);
assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`); assert(konvaObjectGroup, `Object group not found for layer ${rpLayer.id}`);
const transparencyRect = konvaLayer.findOne<Konva.Rect>(`#${getLayerTransparencyRectId(reduxLayer.id)}`); const transparencyRect = konvaLayer.findOne<Konva.Rect>(`#${getRPLayerTransparencyRectId(rpLayer.id)}`);
assert(transparencyRect, `Transparency rect not found for layer ${reduxLayer.id}`); assert(transparencyRect, `Transparency rect not found for layer ${rpLayer.id}`);
// Remove deleted objects // Remove deleted objects
const objectIds = reduxLayer.objects.map(mapId); const objectIds = rpLayer.objects.map(mapId);
for (const objectNode of konvaLayer.find(`.${REGIONAL_PROMPT_LAYER_LINE_NAME}`)) { for (const objectNode of konvaLayer.find(`.${REGIONAL_PROMPT_LAYER_LINE_NAME}`)) {
if (!objectIds.includes(objectNode.id())) { if (!objectIds.includes(objectNode.id())) {
objectNode.destroy(); objectNode.destroy();
} }
} }
for (const reduxObject of reduxLayer.objects) { for (const reduxObject of rpLayer.objects) {
// TODO: Handle rects, images, etc // TODO: Handle rects, images, etc
if (reduxObject.kind !== 'line') { if (reduxObject.kind !== 'line') {
continue; continue;
@ -271,8 +251,8 @@ export const renderLayers = (
konvaObject.stroke(color); konvaObject.stroke(color);
} }
// Only update layer visibility if it has changed. // Only update layer visibility if it has changed.
if (konvaObject.visible() !== reduxLayer.isVisible) { if (konvaObject.visible() !== rpLayer.isVisible) {
konvaObject.visible(reduxLayer.isVisible); konvaObject.visible(rpLayer.isVisible);
} }
} }
@ -282,6 +262,40 @@ export const renderLayers = (
fill: color, fill: color,
opacity: layerOpacity, opacity: layerOpacity,
}); });
};
/**
* Renders the layers on the stage.
* @param stage The konva stage to render on.
* @param reduxLayers Array of the layers from the redux store.
* @param selectedLayerId The selected layer id.
* @param layerOpacity The opacity of the layer.
* @param onLayerPosChanged Callback for when the layer's position changes. This is optional to allow for offscreen rendering.
* @returns
*/
export const renderLayers = (
stage: Konva.Stage,
reduxLayers: Layer[],
selectedLayerId: string | null,
layerOpacity: number,
tool: Tool,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
) => {
const reduxLayerIds = reduxLayers.map(mapId);
// Remove un-rendered layers
for (const konvaLayer of stage.find<Konva.Layer>(`.${REGIONAL_PROMPT_LAYER_NAME}`)) {
if (!reduxLayerIds.includes(konvaLayer.id())) {
konvaLayer.destroy();
}
}
for (let layerIndex = 0; layerIndex < reduxLayers.length; layerIndex++) {
const reduxLayer = reduxLayers[layerIndex];
assert(reduxLayer, `Layer at index ${layerIndex} is undefined`);
if (reduxLayer.kind === 'regionalPromptLayer') {
renderRPLayer(stage, reduxLayer, layerIndex, selectedLayerId, tool, layerOpacity, onLayerPosChanged);
}
} }
}; };
@ -322,7 +336,7 @@ export const renderBbox = (
let rect = konvaLayer.findOne<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`); let rect = konvaLayer.findOne<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`);
if (!rect) { if (!rect) {
rect = new Konva.Rect({ rect = new Konva.Rect({
id: getLayerBboxId(selectedLayerId), id: getPRLayerBboxId(selectedLayerId),
name: REGIONAL_PROMPT_LAYER_BBOX_NAME, name: REGIONAL_PROMPT_LAYER_BBOX_NAME,
strokeWidth: 1, strokeWidth: 1,
}); });