mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): organize layer naming
prep for non-rp layer types
This commit is contained in:
parent
f3b4cecf2e
commit
642a0de3dd
@ -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
|
||||
|
||||
|
@ -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>;
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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}>
|
||||
|
@ -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));
|
||||
|
@ -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 (
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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]));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user