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_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

View File

@ -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 <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 { 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]
);

View File

@ -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]
);

View File

@ -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 (
<Flex gap={2} onClickCapture={onClickCapture} bg={color} borderRadius="base" p="1px" ps={3}>

View File

@ -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));

View File

@ -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 (

View File

@ -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]
);

View File

@ -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]
);

View File

@ -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<Konva.Stage | null>(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]
);

View File

@ -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;
};

View File

@ -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]));
}
}
},

View File

@ -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,15 +80,19 @@ 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<Layer['kind'], string, { uuid: string; color: RgbColor }>) => {
const layer: PromptRegionLayer = {
id: getLayerId(action.meta.uuid),
if (action.payload === 'regionalPromptLayer') {
const layer: RegionalPromptLayer = {
id: getRPLayerId(action.meta.uuid),
isVisible: true,
bbox: null,
kind: action.payload,
@ -102,28 +106,11 @@ export const regionalPromptsSlice = createSlice({
};
state.layers.push(layer);
state.selectedLayer = layer.id;
return;
}
},
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>) => {
state.layers = state.layers.filter((l) => l.id !== action.payload);
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
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 layer = state.layers.find((l) => l.id === layerId);
if (!layer) {
return;
}
if (isRegionalPromptLayer(layer)) {
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;
}
},
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;
}
},
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;
}
},
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;
}
},
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;
}
const lineId = getLayerLineId(layer.id, action.meta.uuid);
if (isRegionalPromptLayer(layer)) {
const lineId = getRPLayerLineId(layer.id, action.meta.uuid);
layer.objects.push({
kind: 'line',
tool: state.tool,
@ -210,20 +212,32 @@ export const regionalPromptsSlice = createSlice({
],
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);
}
},
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>) => {
state.brushSize = action.payload;
},
@ -233,17 +247,7 @@ export const regionalPromptsSlice = createSlice({
promptLayerOpacityChanged: (state, action: PayloadAction<number>) => {
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<RegionalPromptsState> = {
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<RegionalPromptsState
redoType: redoRegionalPrompts.type,
clearHistoryType: clearHistoryRegionalPrompts.type,
groupBy: (action, state, history) => {
// 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<RegionalPromptsState
}
// 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)) {
if (rpLayerBboxChanged.match(action)) {
return false;
}
// 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 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 {
BRUSH_PREVIEW_BORDER_INNER_ID,
BRUSH_PREVIEW_BORDER_OUTER_ID,
BRUSH_PREVIEW_FILL_ID,
BRUSH_PREVIEW_LAYER_ID,
getLayerBboxId,
getLayerObjectGroupId,
getLayerTransparencyRectId,
getPRLayerBboxId,
getRPLayerObjectGroupId,
getRPLayerTransparencyRectId,
REGIONAL_PROMPT_LAYER_BBOX_NAME,
REGIONAL_PROMPT_LAYER_LINE_NAME,
REGIONAL_PROMPT_LAYER_NAME,
@ -125,41 +125,21 @@ export const renderBrushPreview = (
});
};
/**
* 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 = (
const renderRPLayer = (
stage: Konva.Stage,
reduxLayers: Layer[],
rpLayer: RegionalPromptLayer,
rpLayerIndex: number,
selectedLayerId: string | null,
layerOpacity: number,
tool: Tool,
layerOpacity: number,
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`);
let konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
let konvaLayer = stage.findOne<Konva.Layer>(`#${rpLayer.id}`);
if (!konvaLayer) {
// This layer hasn't been added to the konva state yet
konvaLayer = new Konva.Layer({
id: reduxLayer.id,
id: rpLayer.id,
name: REGIONAL_PROMPT_LAYER_NAME,
draggable: true,
});
@ -167,7 +147,7 @@ export const renderLayers = (
// Create a `dragmove` listener for this layer
if (onLayerPosChanged) {
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)
const konvaObjectGroup = new Konva.Group({
id: getLayerObjectGroupId(reduxLayer.id, uuidv4()),
id: getRPLayerObjectGroupId(rpLayer.id, uuidv4()),
name: REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
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
// strokes' color doesn't matter - the only requirement is that they are not transparent.
const transparencyRect = new Konva.Rect({
id: getLayerTransparencyRectId(reduxLayer.id),
id: getRPLayerTransparencyRectId(rpLayer.id),
globalCompositeOperation: 'source-in',
listening: false,
});
@ -215,29 +195,29 @@ export const renderLayers = (
// 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
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: 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}`);
assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`);
const transparencyRect = konvaLayer.findOne<Konva.Rect>(`#${getLayerTransparencyRectId(reduxLayer.id)}`);
assert(transparencyRect, `Transparency rect not found for layer ${reduxLayer.id}`);
assert(konvaObjectGroup, `Object group not found for layer ${rpLayer.id}`);
const transparencyRect = konvaLayer.findOne<Konva.Rect>(`#${getRPLayerTransparencyRectId(rpLayer.id)}`);
assert(transparencyRect, `Transparency rect not found for layer ${rpLayer.id}`);
// 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}`)) {
if (!objectIds.includes(objectNode.id())) {
objectNode.destroy();
}
}
for (const reduxObject of reduxLayer.objects) {
for (const reduxObject of rpLayer.objects) {
// TODO: Handle rects, images, etc
if (reduxObject.kind !== 'line') {
continue;
@ -271,8 +251,8 @@ export const renderLayers = (
konvaObject.stroke(color);
}
// Only update layer visibility if it has changed.
if (konvaObject.visible() !== reduxLayer.isVisible) {
konvaObject.visible(reduxLayer.isVisible);
if (konvaObject.visible() !== rpLayer.isVisible) {
konvaObject.visible(rpLayer.isVisible);
}
}
@ -282,6 +262,40 @@ export const renderLayers = (
fill: color,
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}`);
if (!rect) {
rect = new Konva.Rect({
id: getLayerBboxId(selectedLayerId),
id: getPRLayerBboxId(selectedLayerId),
name: REGIONAL_PROMPT_LAYER_BBOX_NAME,
strokeWidth: 1,
});