From 642a0de3dd1ca271554b832e4215cace074e7503 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 19 Apr 2024 15:18:26 +1000
Subject: [PATCH] feat(ui): organize layer naming
prep for non-rp layer types
---
.../util/graph/addRegionalPromptsToGraph.ts | 3 +-
.../components/AddLayerButton.tsx | 2 +-
.../components/LayerAutoNegativeCombobox.tsx | 7 +-
.../components/LayerColorPicker.tsx | 7 +-
.../components/LayerListItem.tsx | 13 +-
.../regionalPrompts/components/LayerMenu.tsx | 4 +-
.../components/LayerVisibilityToggle.tsx | 4 +-
.../RegionalPromptsNegativePrompt.tsx | 4 +-
.../RegionalPromptsPositivePrompt.tsx | 4 +-
.../components/StageComponent.tsx | 17 +-
.../regionalPrompts/hooks/layerStateHooks.ts | 32 +-
.../regionalPrompts/hooks/mouseEventHooks.ts | 10 +-
.../store/regionalPromptsSlice.ts | 267 ++++++++---------
.../regionalPrompts/util/renderers.ts | 278 +++++++++---------
14 files changed, 340 insertions(+), 312 deletions(-)
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addRegionalPromptsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addRegionalPromptsToGraph.ts
index 03e88bcd5c..263dcde55e 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/addRegionalPromptsToGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/addRegionalPromptsToGraph.ts
@@ -11,6 +11,7 @@ import {
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
PROMPT_REGION_POSITIVE_COND_PREFIX,
} from 'features/nodes/util/graph/constants';
+import { isRegionalPromptLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs';
import { size } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
@@ -22,7 +23,7 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
// TODO: Handle non-SDXL
// const isSDXL = state.generation.model?.base === 'sdxl';
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.negativePrompt || l.positivePrompt); // Only layers with prompts get added to the graph
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx
index 01107489b7..1791f73cce 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx
@@ -8,7 +8,7 @@ export const AddLayerButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
- dispatch(layerAdded('promptRegionLayer'));
+ dispatch(layerAdded('regionalPromptLayer'));
}, [dispatch]);
return ;
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerAutoNegativeCombobox.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerAutoNegativeCombobox.tsx
index 0f29b41287..cf98e639e5 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerAutoNegativeCombobox.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerAutoNegativeCombobox.tsx
@@ -4,7 +4,8 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import {
- layerAutoNegativeChanged,
+ isRegionalPromptLayer,
+ rpLayerAutoNegativeChanged,
selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react';
@@ -25,7 +26,7 @@ const useAutoNegative = (layerId: string) => {
() =>
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
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;
}),
[layerId]
@@ -44,7 +45,7 @@ const AutoNegativeCombobox = ({ layerId }: Props) => {
if (!isParameterAutoNegative(v?.value)) {
return;
}
- dispatch(layerAutoNegativeChanged({ layerId, autoNegative: v.value }));
+ dispatch(rpLayerAutoNegativeChanged({ layerId, autoNegative: v.value }));
},
[dispatch, layerId]
);
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx
index 17012623ae..1ac6706d04 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx
@@ -3,7 +3,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import {
- promptRegionLayerColorChanged,
+ isRegionalPromptLayer,
+ rpLayerColorChanged,
selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react';
@@ -20,7 +21,7 @@ export const LayerColorPicker = memo(({ id }: Props) => {
() =>
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
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;
}),
[id]
@@ -29,7 +30,7 @@ export const LayerColorPicker = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const onColorChange = useCallback(
(color: RgbColor) => {
- dispatch(promptRegionLayerColorChanged({ layerId: id, color }));
+ dispatch(rpLayerColorChanged({ layerId: id, color }));
},
[dispatch, id]
);
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx
index e9ae02bd34..c383e35314 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx
@@ -7,8 +7,9 @@ import { LayerMenu } from 'features/regionalPrompts/components/LayerMenu';
import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle';
import { RegionalPromptsNegativePrompt } from 'features/regionalPrompts/components/RegionalPromptsNegativePrompt';
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 { assert } from 'tsafe';
type Props = {
id: string;
@@ -18,15 +19,13 @@ export const LayerListItem = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const selectedLayer = useAppSelector((s) => s.regionalPrompts.present.selectedLayer);
const color = useAppSelector((s) => {
- const color = s.regionalPrompts.present.layers.find((l) => l.id === id)?.color;
- if (color) {
- return rgbaColorToString({ ...color, a: selectedLayer === id ? 1 : 0.35 });
- }
- return 'base.700';
+ const layer = s.regionalPrompts.present.layers.find((l) => l.id === id);
+ assert(isRegionalPromptLayer(layer), `Layer ${id} not found or not an RP layer`);
+ return rgbaColorToString({ ...layer.color, a: selectedLayer === id ? 1 : 0.35 });
});
const onClickCapture = useCallback(() => {
// Must be capture so that the layer is selected before deleting/resetting/etc
- dispatch(layerSelected(id));
+ dispatch(rpLayerSelected(id));
}, [dispatch, id]);
return (
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerMenu.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerMenu.tsx
index 7091cc63ab..c69fff907c 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerMenu.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerMenu.tsx
@@ -7,7 +7,7 @@ import {
layerMovedForward,
layerMovedToBack,
layerMovedToFront,
- layerReset,
+ rpLayerReset,
selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react';
@@ -55,7 +55,7 @@ export const LayerMenu = memo(({ id }: Props) => {
dispatch(layerMovedToBack(id));
}, [dispatch, id]);
const resetLayer = useCallback(() => {
- dispatch(layerReset(id));
+ dispatch(rpLayerReset(id));
}, [dispatch, id]);
const deleteLayer = useCallback(() => {
dispatch(layerDeleted(id));
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerVisibilityToggle.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerVisibilityToggle.tsx
index 2a004f262a..a4c96ddea8 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerVisibilityToggle.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerVisibilityToggle.tsx
@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
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 { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi';
@@ -13,7 +13,7 @@ export const LayerVisibilityToggle = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isVisible = useLayerIsVisible(id);
const onClick = useCallback(() => {
- dispatch(layerIsVisibleToggled(id));
+ dispatch(rpLayerIsVisibleToggled(id));
}, [dispatch, id]);
return (
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsNegativePrompt.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsNegativePrompt.tsx
index 31863643a0..05e94304a2 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsNegativePrompt.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsNegativePrompt.tsx
@@ -5,7 +5,7 @@ import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
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 type { HotkeyCallback } from 'react-hotkeys-hook';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -22,7 +22,7 @@ export const RegionalPromptsNegativePrompt = memo((props: Props) => {
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
- dispatch(negativePromptChanged({ layerId: props.layerId, prompt: v }));
+ dispatch(rpLayerNegativePromptChanged({ layerId: props.layerId, prompt: v }));
},
[dispatch, props.layerId]
);
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPositivePrompt.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPositivePrompt.tsx
index 2592edf0e8..84cc6c1f57 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPositivePrompt.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPositivePrompt.tsx
@@ -5,7 +5,7 @@ import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
import { usePrompt } from 'features/prompt/usePrompt';
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 type { HotkeyCallback } from 'react-hotkeys-hook';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -22,7 +22,7 @@ export const RegionalPromptsPositivePrompt = memo((props: Props) => {
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
- dispatch(positivePromptChanged({ layerId: props.layerId, prompt: v }));
+ dispatch(rpLayerPositivePromptChanged({ layerId: props.layerId, prompt: v }));
},
[dispatch, props.layerId]
);
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx
index 7003c6bbd0..72d2a60c59 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx
+++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx
@@ -6,8 +6,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks';
import {
$cursorPosition,
- layerBboxChanged,
- layerTranslated,
+ isRegionalPromptLayer,
+ rpLayerBboxChanged,
+ rpLayerTranslated,
selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
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 { atom } from 'nanostores';
import { useCallback, useLayoutEffect } from 'react';
+import { assert } from 'tsafe';
const log = logger('regionalPrompts');
const $stage = atom(null);
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) => {
@@ -34,14 +41,14 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
const onLayerPosChanged = useCallback(
(layerId: string, x: number, y: number) => {
- dispatch(layerTranslated({ layerId, x, y }));
+ dispatch(rpLayerTranslated({ layerId, x, y }));
},
[dispatch]
);
const onBboxChanged = useCallback(
(layerId: string, bbox: IRect) => {
- dispatch(layerBboxChanged({ layerId, bbox }));
+ dispatch(rpLayerBboxChanged({ layerId, bbox }));
},
[dispatch]
);
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/layerStateHooks.ts
index 51c9793add..8e82142994 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/hooks/layerStateHooks.ts
+++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/layerStateHooks.ts
@@ -1,47 +1,47 @@
import { createSelector } from '@reduxjs/toolkit';
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 { assert } from 'tsafe';
export const useLayerPositivePrompt = (layerId: string) => {
const selectLayer = useMemo(
() =>
- createSelector(
- selectRegionalPromptsSlice,
- (regionalPrompts) => regionalPrompts.present.layers.find((l) => l.id === layerId)?.positivePrompt
- ),
+ createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
+ const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
+ assert(isRegionalPromptLayer(layer), `Layer ${layerId} not found or not an RP layer`);
+ return layer.positivePrompt;
+ }),
[layerId]
);
const prompt = useAppSelector(selectLayer);
- assert(prompt !== undefined, `Layer ${layerId} doesn't exist!`);
return prompt;
};
export const useLayerNegativePrompt = (layerId: string) => {
const selectLayer = useMemo(
() =>
- createSelector(
- selectRegionalPromptsSlice,
- (regionalPrompts) => regionalPrompts.present.layers.find((l) => l.id === layerId)?.negativePrompt
- ),
+ createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
+ const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
+ assert(isRegionalPromptLayer(layer), `Layer ${layerId} not found or not an RP layer`);
+ return layer.negativePrompt;
+ }),
[layerId]
);
const prompt = useAppSelector(selectLayer);
- assert(prompt !== undefined, `Layer ${layerId} doesn't exist!`);
return prompt;
};
export const useLayerIsVisible = (layerId: string) => {
const selectLayer = useMemo(
() =>
- createSelector(
- selectRegionalPromptsSlice,
- (regionalPrompts) => regionalPrompts.present.layers.find((l) => l.id === layerId)?.isVisible
- ),
+ createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
+ const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
+ assert(isRegionalPromptLayer(layer), `Layer ${layerId} not found or not an RP layer`);
+ return layer.isVisible;
+ }),
[layerId]
);
const isVisible = useAppSelector(selectLayer);
- assert(isVisible !== undefined, `Layer ${layerId} doesn't exist!`);
return isVisible;
};
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts
index 80a3620b0f..ad08f58e8d 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts
+++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts
@@ -5,8 +5,8 @@ import {
$cursorPosition,
$isMouseDown,
$isMouseOver,
- lineAdded,
- pointsAdded,
+ rpLayerLineAdded,
+ rpLayerPointsAdded,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
@@ -43,7 +43,7 @@ export const useMouseEvents = () => {
$isMouseDown.set(true);
const tool = getTool();
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]
@@ -75,7 +75,7 @@ export const useMouseEvents = () => {
}
const tool = getTool();
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
- dispatch(pointsAdded([pos.x, pos.y]));
+ dispatch(rpLayerPointsAdded([pos.x, pos.y]));
}
},
[dispatch]
@@ -111,7 +111,7 @@ export const useMouseEvents = () => {
$isMouseDown.set(true);
const tool = getTool();
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]));
}
}
},
diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts
index 65a9fab5ab..9645a547dd 100644
--- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts
+++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts
@@ -44,14 +44,14 @@ type LayerObject = ImageObject | LineObject | FillRectObject;
type LayerBase = {
id: string;
+};
+
+export type RegionalPromptLayer = LayerBase & {
isVisible: boolean;
x: number;
y: number;
bbox: IRect | null;
-};
-
-type PromptRegionLayer = LayerBase & {
- kind: 'promptRegionLayer';
+ kind: 'regionalPromptLayer';
objects: LayerObject[];
positivePrompt: string;
negativePrompt: string;
@@ -59,13 +59,13 @@ type PromptRegionLayer = LayerBase & {
autoNegative: ParameterAutoNegative;
};
-export type Layer = PromptRegionLayer;
+export type Layer = RegionalPromptLayer;
type RegionalPromptsState = {
_version: 1;
tool: Tool;
selectedLayer: string | null;
- layers: PromptRegionLayer[];
+ layers: Layer[];
brushSize: number;
promptLayerOpacity: number;
};
@@ -80,50 +80,37 @@ export const initialRegionalPromptsState: RegionalPromptsState = {
};
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({
name: 'regionalPrompts',
initialState: initialRegionalPromptsState,
reducers: {
+ //#region Meta Layer
layerAdded: {
reducer: (state, action: PayloadAction) => {
- const layer: PromptRegionLayer = {
- id: getLayerId(action.meta.uuid),
- isVisible: true,
- bbox: null,
- kind: action.payload,
- positivePrompt: '',
- negativePrompt: '',
- objects: [],
- color: action.meta.color,
- x: 0,
- y: 0,
- autoNegative: 'off',
- };
- state.layers.push(layer);
- state.selectedLayer = layer.id;
+ if (action.payload === 'regionalPromptLayer') {
+ const layer: RegionalPromptLayer = {
+ id: getRPLayerId(action.meta.uuid),
+ isVisible: true,
+ bbox: null,
+ kind: action.payload,
+ positivePrompt: '',
+ negativePrompt: '',
+ objects: [],
+ color: action.meta.color,
+ x: 0,
+ y: 0,
+ autoNegative: 'off',
+ };
+ state.layers.push(layer);
+ state.selectedLayer = layer.id;
+ return;
+ }
},
prepare: (payload: Layer['kind']) => ({ payload, meta: { uuid: uuidv4(), color: LayerColors.next() } }),
},
- layerSelected: (state, action: PayloadAction) => {
- state.selectedLayer = action.payload;
- },
- layerIsVisibleToggled: (state, action: PayloadAction) => {
- const layer = state.layers.find((l) => l.id === action.payload);
- if (!layer) {
- return;
- }
- layer.isVisible = !layer.isVisible;
- },
- layerReset: (state, action: PayloadAction) => {
- 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) => {
state.layers = state.layers.filter((l) => l.id !== action.payload);
state.selectedLayer = state.layers[0]?.id ?? null;
@@ -146,84 +133,111 @@ export const regionalPromptsSlice = createSlice({
// Because the layers are in reverse order, moving to the back is equivalent to moving to the front
moveToFront(state.layers, cb);
},
- layerTranslated: (state, action: PayloadAction<{ layerId: string; x: number; y: number }>) => {
+ //#endregion
+ //#region RP Layers
+ rpLayerSelected: (state, action: PayloadAction) => {
+ const layer = state.layers.find((l) => l.id === action.payload);
+ if (isRegionalPromptLayer(layer)) {
+ state.selectedLayer = layer.id;
+ }
+ },
+ rpLayerIsVisibleToggled: (state, action: PayloadAction) => {
+ const layer = state.layers.find((l) => l.id === action.payload);
+ if (isRegionalPromptLayer(layer)) {
+ layer.isVisible = !layer.isVisible;
+ }
+ },
+ rpLayerReset: (state, action: PayloadAction) => {
+ 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 layer = state.layers.find((l) => l.id === layerId);
- if (!layer) {
- return;
+ if (isRegionalPromptLayer(layer)) {
+ layer.x = x;
+ layer.y = y;
}
- layer.x = x;
- 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 layer = state.layers.find((l) => l.id === layerId);
- if (!layer) {
- return;
+ if (isRegionalPromptLayer(layer)) {
+ layer.bbox = bbox;
}
- layer.bbox = bbox;
},
allLayersDeleted: (state) => {
state.layers = [];
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 layer = state.layers.find((l) => l.id === layerId);
- if (!layer) {
- return;
+ if (isRegionalPromptLayer(layer)) {
+ 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 layer = state.layers.find((l) => l.id === layerId);
- if (!layer) {
- return;
+ if (isRegionalPromptLayer(layer)) {
+ 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 layer = state.layers.find((l) => l.id === layerId);
- if (!layer || layer.kind !== 'promptRegionLayer') {
- return;
+ if (isRegionalPromptLayer(layer)) {
+ layer.color = color;
}
- layer.color = color;
},
- lineAdded: {
+ rpLayerLineAdded: {
reducer: (state, action: PayloadAction<[number, number, number, number], string, { uuid: string }>) => {
const layer = state.layers.find((l) => l.id === state.selectedLayer);
- if (!layer || layer.kind !== 'promptRegionLayer') {
- return;
+ if (isRegionalPromptLayer(layer)) {
+ const lineId = getRPLayerLineId(layer.id, action.meta.uuid);
+ layer.objects.push({
+ kind: 'line',
+ tool: state.tool,
+ id: lineId,
+ points: [
+ action.payload[0] - layer.x,
+ action.payload[1] - layer.y,
+ action.payload[2] - layer.x,
+ action.payload[3] - layer.y,
+ ],
+ strokeWidth: state.brushSize,
+ });
}
- const lineId = getLayerLineId(layer.id, action.meta.uuid);
- layer.objects.push({
- kind: 'line',
- tool: state.tool,
- id: lineId,
- points: [
- action.payload[0] - layer.x,
- action.payload[1] - layer.y,
- action.payload[2] - layer.x,
- action.payload[3] - layer.y,
- ],
- strokeWidth: state.brushSize,
- });
},
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);
- if (!layer || layer.kind !== 'promptRegionLayer') {
- return;
+ if (isRegionalPromptLayer(layer)) {
+ const lastLine = layer.objects.findLast(isLine);
+ if (!lastLine) {
+ return;
+ }
+ lastLine.points.push(action.payload[0] - layer.x, action.payload[1] - layer.y);
}
- const lastLine = layer.objects.findLast(isLine);
- if (!lastLine) {
- return;
- }
- 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) => {
state.brushSize = action.payload;
},
@@ -233,17 +247,7 @@ export const regionalPromptsSlice = createSlice({
promptLayerOpacityChanged: (state, action: PayloadAction) => {
state.promptLayerOpacity = action.payload;
},
- layerAutoNegativeChanged: (
- 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;
- },
+ //#endregion
},
});
@@ -272,27 +276,28 @@ class LayerColors {
}
export const {
- layerAdded,
- layerSelected,
- layerReset,
- layerDeleted,
- layerIsVisibleToggled,
- positivePromptChanged,
- negativePromptChanged,
- lineAdded,
- pointsAdded,
- promptRegionLayerColorChanged,
- brushSizeChanged,
- layerMovedForward,
- layerMovedToFront,
- layerMovedBackward,
- layerMovedToBack,
- toolChanged,
- layerTranslated,
- layerBboxChanged,
- promptLayerOpacityChanged,
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;
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';
// Getters for non-singleton layer and object IDs
-const getLayerId = (layerId: string) => `layer_${layerId}`;
-const getLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
-export const getLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
-export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
-export const getLayerTransparencyRectId = (layerId: string) => `${layerId}.transparency_rect`;
+const getRPLayerId = (layerId: string) => `rp_layer_${layerId}`;
+const getRPLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
+export const getRPLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
+export const getPRLayerBboxId = (layerId: string) => `${layerId}.bbox`;
+export const getRPLayerTransparencyRectId = (layerId: string) => `${layerId}.transparency_rect`;
export const regionalPromptsPersistConfig: PersistConfig = {
name: regionalPromptsSlice.name,
@@ -339,11 +344,11 @@ export const clearHistoryRegionalPrompts = createAction(`${regionalPromptsSlice.
// These actions are _individually_ grouped together as single undoable actions
const undoableGroupByMatcher = isAnyOf(
- positivePromptChanged,
- negativePromptChanged,
brushSizeChanged,
- layerTranslated,
- promptLayerOpacityChanged
+ promptLayerOpacityChanged,
+ rpLayerPositivePromptChanged,
+ rpLayerNegativePromptChanged,
+ rpLayerTranslated
);
const LINE_1 = 'LINE_1';
@@ -355,13 +360,13 @@ export const regionalPromptsUndoableConfig: UndoableOptions {
- // Lines are started with `lineAdded` and may have any number of subsequent `pointsAdded` events. We can use a
- // double-buffering-ish trick to group each logical line as a single undoable action, without grouping separate
- // logical lines as a single undo action.
- if (lineAdded.match(action)) {
+ // Lines are started with `rpLayerLineAdded` and may have any number of subsequent `rpLayerPointsAdded` events.
+ // We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping
+ // separate logical lines as a single undo action.
+ if (rpLayerLineAdded.match(action)) {
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) {
return history.group;
}
@@ -378,7 +383,7 @@ export const regionalPromptsUndoableConfig: UndoableOptions void
+) => {
+ let konvaLayer = stage.findOne(`#${rpLayer.id}`);
+
+ if (!konvaLayer) {
+ // This layer hasn't been added to the konva state yet
+ konvaLayer = new Konva.Layer({
+ id: rpLayer.id,
+ name: REGIONAL_PROMPT_LAYER_NAME,
+ draggable: true,
+ });
+
+ // Create a `dragmove` listener for this layer
+ if (onLayerPosChanged) {
+ konvaLayer.on('dragend', function (e) {
+ onLayerPosChanged(rpLayer.id, e.target.x(), e.target.y());
+ });
+ }
+
+ // The dragBoundFunc limits how far the layer can be dragged
+ konvaLayer.dragBoundFunc(function (pos) {
+ const cursorPos = getScaledCursorPosition(stage);
+ if (!cursorPos) {
+ return this.getAbsolutePosition();
+ }
+ // Prevent the user from dragging the layer out of 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: getRPLayerObjectGroupId(rpLayer.id, uuidv4()),
+ name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
+ listening: false,
+ });
+ konvaLayer.add(konvaObjectGroup);
+
+ // To achieve performant transparency, we use the `source-in` blending mode on a rect that covers the entire layer.
+ // 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.
+ const transparencyRect = new Konva.Rect({
+ id: getRPLayerTransparencyRectId(rpLayer.id),
+ globalCompositeOperation: 'source-in',
+ listening: false,
+ });
+ konvaLayer.add(transparencyRect);
+
+ stage.add(konvaLayer);
+
+ // When a layer is added, it ends up on top of the brush preview - we need to move the preview back to the top.
+ stage.findOne(`#${BRUSH_PREVIEW_LAYER_ID}`)?.moveToTop();
+ }
+
+ // Update the layer's position and listening state (only the selected layer is listening)
+ konvaLayer.setAttrs({
+ listening: rpLayer.id === selectedLayerId && tool === 'move',
+ x: rpLayer.x,
+ y: rpLayer.y,
+ // 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.
+ zIndex: rpLayerIndex,
+ });
+
+ const color = rgbColorToString(rpLayer.color);
+ const konvaObjectGroup = konvaLayer.findOne(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`);
+ assert(konvaObjectGroup, `Object group not found for layer ${rpLayer.id}`);
+ const transparencyRect = konvaLayer.findOne(`#${getRPLayerTransparencyRectId(rpLayer.id)}`);
+ assert(transparencyRect, `Transparency rect not found for layer ${rpLayer.id}`);
+
+ // Remove deleted objects
+ const objectIds = rpLayer.objects.map(mapId);
+ for (const objectNode of konvaLayer.find(`.${REGIONAL_PROMPT_LAYER_LINE_NAME}`)) {
+ if (!objectIds.includes(objectNode.id())) {
+ objectNode.destroy();
+ }
+ }
+
+ for (const reduxObject of rpLayer.objects) {
+ // TODO: Handle rects, images, etc
+ if (reduxObject.kind !== 'line') {
+ continue;
+ }
+
+ let konvaObject = stage.findOne(`#${reduxObject.id}`);
+
+ if (!konvaObject) {
+ // This object hasn't been added to the konva state yet.
+ konvaObject = new Konva.Line({
+ id: reduxObject.id,
+ key: reduxObject.id,
+ name: REGIONAL_PROMPT_LAYER_LINE_NAME,
+ strokeWidth: reduxObject.strokeWidth,
+ tension: 0,
+ lineCap: 'round',
+ lineJoin: 'round',
+ shadowForStrokeEnabled: false,
+ globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
+ listening: false,
+ });
+ konvaObjectGroup.add(konvaObject);
+ }
+
+ // Only update the points if they have changed. The point values are never mutated, they are only added to the array.
+ if (konvaObject.points().length !== reduxObject.points.length) {
+ konvaObject.points(reduxObject.points);
+ }
+ // Only update the color if it has changed.
+ if (konvaObject.stroke() !== color) {
+ konvaObject.stroke(color);
+ }
+ // Only update layer visibility if it has changed.
+ if (konvaObject.visible() !== rpLayer.isVisible) {
+ konvaObject.visible(rpLayer.isVisible);
+ }
+ }
+
+ // Set the layer opacity - must happen after all objects are added to the layer so the rect is the right size
+ transparencyRect.setAttrs({
+ ...konvaLayer.getClientRect({ skipTransform: true }),
+ fill: color,
+ opacity: layerOpacity,
+ });
+};
+
/**
* Renders the layers on the stage.
* @param stage The konva stage to render on.
@@ -154,134 +293,9 @@ export const renderLayers = (
for (let layerIndex = 0; layerIndex < reduxLayers.length; layerIndex++) {
const reduxLayer = reduxLayers[layerIndex];
assert(reduxLayer, `Layer at index ${layerIndex} is undefined`);
- let konvaLayer = stage.findOne(`#${reduxLayer.id}`);
-
- if (!konvaLayer) {
- // This layer hasn't been added to the konva state yet
- konvaLayer = new Konva.Layer({
- id: reduxLayer.id,
- name: REGIONAL_PROMPT_LAYER_NAME,
- draggable: true,
- });
-
- // Create a `dragmove` listener for this layer
- if (onLayerPosChanged) {
- konvaLayer.on('dragend', function (e) {
- onLayerPosChanged(reduxLayer.id, e.target.x(), e.target.y());
- });
- }
-
- // The dragBoundFunc limits how far the layer can be dragged
- konvaLayer.dragBoundFunc(function (pos) {
- const cursorPos = getScaledCursorPosition(stage);
- if (!cursorPos) {
- return this.getAbsolutePosition();
- }
- // Prevent the user from dragging the layer out of 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: getLayerObjectGroupId(reduxLayer.id, uuidv4()),
- name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
- listening: false,
- });
- konvaLayer.add(konvaObjectGroup);
-
- // To achieve performant transparency, we use the `source-in` blending mode on a rect that covers the entire layer.
- // 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.
- const transparencyRect = new Konva.Rect({
- id: getLayerTransparencyRectId(reduxLayer.id),
- globalCompositeOperation: 'source-in',
- listening: false,
- });
- konvaLayer.add(transparencyRect);
-
- stage.add(konvaLayer);
-
- // When a layer is added, it ends up on top of the brush preview - we need to move the preview back to the top.
- stage.findOne(`#${BRUSH_PREVIEW_LAYER_ID}`)?.moveToTop();
+ if (reduxLayer.kind === 'regionalPromptLayer') {
+ renderRPLayer(stage, reduxLayer, layerIndex, selectedLayerId, tool, layerOpacity, onLayerPosChanged);
}
-
- // Update the layer's position and listening state (only the selected layer is listening)
- konvaLayer.setAttrs({
- listening: reduxLayer.id === selectedLayerId && tool === 'move',
- x: reduxLayer.x,
- y: reduxLayer.y,
- // There are reduxLayers.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.
- zIndex: layerIndex,
- });
-
- const color = rgbColorToString(reduxLayer.color);
- const konvaObjectGroup = konvaLayer.findOne(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`);
- assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`);
- const transparencyRect = konvaLayer.findOne(`#${getLayerTransparencyRectId(reduxLayer.id)}`);
- assert(transparencyRect, `Transparency rect not found for layer ${reduxLayer.id}`);
-
- // Remove deleted objects
- const objectIds = reduxLayer.objects.map(mapId);
- for (const objectNode of konvaLayer.find(`.${REGIONAL_PROMPT_LAYER_LINE_NAME}`)) {
- if (!objectIds.includes(objectNode.id())) {
- objectNode.destroy();
- }
- }
-
- for (const reduxObject of reduxLayer.objects) {
- // TODO: Handle rects, images, etc
- if (reduxObject.kind !== 'line') {
- continue;
- }
-
- let konvaObject = stage.findOne(`#${reduxObject.id}`);
-
- if (!konvaObject) {
- // This object hasn't been added to the konva state yet.
- konvaObject = new Konva.Line({
- id: reduxObject.id,
- key: reduxObject.id,
- name: REGIONAL_PROMPT_LAYER_LINE_NAME,
- strokeWidth: reduxObject.strokeWidth,
- tension: 0,
- lineCap: 'round',
- lineJoin: 'round',
- shadowForStrokeEnabled: false,
- globalCompositeOperation: reduxObject.tool === 'brush' ? 'source-over' : 'destination-out',
- listening: false,
- });
- konvaObjectGroup.add(konvaObject);
- }
-
- // Only update the points if they have changed. The point values are never mutated, they are only added to the array.
- if (konvaObject.points().length !== reduxObject.points.length) {
- konvaObject.points(reduxObject.points);
- }
- // Only update the color if it has changed.
- if (konvaObject.stroke() !== color) {
- konvaObject.stroke(color);
- }
- // Only update layer visibility if it has changed.
- if (konvaObject.visible() !== reduxLayer.isVisible) {
- konvaObject.visible(reduxLayer.isVisible);
- }
- }
-
- // Set the layer opacity - must happen after all objects are added to the layer so the rect is the right size
- transparencyRect.setAttrs({
- ...konvaLayer.getClientRect({ skipTransform: true }),
- fill: color,
- opacity: layerOpacity,
- });
}
};
@@ -322,7 +336,7 @@ export const renderBbox = (
let rect = konvaLayer.findOne(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`);
if (!rect) {
rect = new Konva.Rect({
- id: getLayerBboxId(selectedLayerId),
+ id: getPRLayerBboxId(selectedLayerId),
name: REGIONAL_PROMPT_LAYER_BBOX_NAME,
strokeWidth: 1,
});