mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
refactor(ui): revise regional prompts state to support prompt-less mask layers
This structure is more adaptable to future features like IP-Adapter-only regions, controlnet layers, image masks, etc.
This commit is contained in:
parent
a00e703144
commit
58d3a9e7d4
@ -26,7 +26,7 @@ const sx: ChakraProps['sx'] = {
|
||||
|
||||
const colorPickerStyles: CSSProperties = { width: '100%' };
|
||||
|
||||
const numberInputWidth: ChakraProps['w'] = '4.2rem';
|
||||
const numberInputWidth: ChakraProps['w'] = '3.5rem';
|
||||
|
||||
const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
const { color, onChange, withNumberInput, ...rest } = props;
|
||||
@ -41,7 +41,7 @@ const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
{withNumberInput && (
|
||||
<Flex gap={5}>
|
||||
<FormControl gap={0}>
|
||||
<FormLabel>{t('common.red')}</FormLabel>
|
||||
<FormLabel>{t('common.red')[0]}</FormLabel>
|
||||
<CompositeNumberInput
|
||||
value={color.r}
|
||||
onChange={handleChangeR}
|
||||
@ -53,7 +53,7 @@ const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl gap={0}>
|
||||
<FormLabel>{t('common.green')}</FormLabel>
|
||||
<FormLabel>{t('common.green')[0]}</FormLabel>
|
||||
<CompositeNumberInput
|
||||
value={color.g}
|
||||
onChange={handleChangeG}
|
||||
@ -65,7 +65,7 @@ const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl gap={0}>
|
||||
<FormLabel>{t('common.blue')}</FormLabel>
|
||||
<FormLabel>{t('common.blue')[0]}</FormLabel>
|
||||
<CompositeNumberInput
|
||||
value={color.b}
|
||||
onChange={handleChangeB}
|
||||
@ -77,7 +77,7 @@ const IAIColorPicker = (props: IAIColorPickerProps) => {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl gap={0}>
|
||||
<FormLabel>{t('common.alpha')}</FormLabel>
|
||||
<FormLabel>{t('common.alpha')[0]}</FormLabel>
|
||||
<CompositeNumberInput
|
||||
value={color.a}
|
||||
onChange={handleChangeA}
|
||||
|
@ -1,84 +0,0 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { RgbColorPicker as ColorfulRgbColorPicker } from 'react-colorful';
|
||||
import type { ColorPickerBaseProps, RgbColor } from 'react-colorful/dist/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type RgbColorPickerProps = ColorPickerBaseProps<RgbColor> & {
|
||||
withNumberInput?: boolean;
|
||||
};
|
||||
|
||||
const colorPickerPointerStyles: NonNullable<ChakraProps['sx']> = {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderColor: 'base.100',
|
||||
};
|
||||
|
||||
const sx: ChakraProps['sx'] = {
|
||||
'.react-colorful__hue-pointer': colorPickerPointerStyles,
|
||||
'.react-colorful__saturation-pointer': colorPickerPointerStyles,
|
||||
'.react-colorful__alpha-pointer': colorPickerPointerStyles,
|
||||
gap: 5,
|
||||
flexDir: 'column',
|
||||
};
|
||||
|
||||
const colorPickerStyles: CSSProperties = { width: '100%' };
|
||||
|
||||
const numberInputWidth: ChakraProps['w'] = '4.2rem';
|
||||
|
||||
const RgbColorPicker = (props: RgbColorPickerProps) => {
|
||||
const { color, onChange, withNumberInput, ...rest } = props;
|
||||
const { t } = useTranslation();
|
||||
const handleChangeR = useCallback((r: number) => onChange({ ...color, r }), [color, onChange]);
|
||||
const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]);
|
||||
const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]);
|
||||
return (
|
||||
<Flex sx={sx}>
|
||||
<ColorfulRgbColorPicker color={color} onChange={onChange} style={colorPickerStyles} {...rest} />
|
||||
{withNumberInput && (
|
||||
<Flex gap={5}>
|
||||
<FormControl gap={0}>
|
||||
<FormLabel>{t('common.red')}</FormLabel>
|
||||
<CompositeNumberInput
|
||||
value={color.r}
|
||||
onChange={handleChangeR}
|
||||
min={0}
|
||||
max={255}
|
||||
step={1}
|
||||
w={numberInputWidth}
|
||||
defaultValue={90}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl gap={0}>
|
||||
<FormLabel>{t('common.green')}</FormLabel>
|
||||
<CompositeNumberInput
|
||||
value={color.g}
|
||||
onChange={handleChangeG}
|
||||
min={0}
|
||||
max={255}
|
||||
step={1}
|
||||
w={numberInputWidth}
|
||||
defaultValue={90}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl gap={0}>
|
||||
<FormLabel>{t('common.blue')}</FormLabel>
|
||||
<CompositeNumberInput
|
||||
value={color.b}
|
||||
onChange={handleChangeB}
|
||||
min={0}
|
||||
max={255}
|
||||
step={1}
|
||||
w={numberInputWidth}
|
||||
defaultValue={255}
|
||||
/>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RgbColorPicker);
|
@ -11,7 +11,7 @@ import {
|
||||
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
|
||||
PROMPT_REGION_POSITIVE_COND_PREFIX,
|
||||
} from 'features/nodes/util/graph/constants';
|
||||
import { isRPLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { isVectorMaskLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs';
|
||||
import { size } from 'lodash-es';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
@ -23,12 +23,19 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
||||
return;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
// TODO: Handle non-SDXL
|
||||
const isSDXL = state.generation.model?.base === 'sdxl';
|
||||
const layers = state.regionalPrompts.present.layers
|
||||
.filter(isRPLayer) // 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
|
||||
// Only support vector mask layers now
|
||||
// TODO: Image masks
|
||||
.filter(isVectorMaskLayer)
|
||||
// Only visible layers are rendered on the canvas
|
||||
.filter((l) => l.isVisible)
|
||||
// Only layers with prompts get added to the graph
|
||||
.filter((l) => {
|
||||
const hasTextPrompt = l.textPrompt && (l.textPrompt.positive || l.textPrompt.negative);
|
||||
const hasAtLeastOneImagePrompt = l.imagePrompts.length > 0;
|
||||
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
||||
});
|
||||
|
||||
const layerIds = layers.map((l) => l.id);
|
||||
const blobs = await getRegionalPromptLayerBlobs(layerIds);
|
||||
@ -123,19 +130,19 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
||||
};
|
||||
graph.nodes[maskToTensorNode.id] = maskToTensorNode;
|
||||
|
||||
if (layer.positivePrompt) {
|
||||
if (layer.textPrompt?.positive) {
|
||||
// The main positive conditioning node
|
||||
const regionalPositiveCondNode: S['SDXLCompelPromptInvocation'] | S['CompelInvocation'] = isSDXL
|
||||
? {
|
||||
type: 'sdxl_compel_prompt',
|
||||
id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`,
|
||||
prompt: layer.positivePrompt,
|
||||
style: layer.positivePrompt, // TODO: Should we put the positive prompt in both fields?
|
||||
prompt: layer.textPrompt.positive,
|
||||
style: layer.textPrompt.positive, // TODO: Should we put the positive prompt in both fields?
|
||||
}
|
||||
: {
|
||||
type: 'compel',
|
||||
id: `${PROMPT_REGION_POSITIVE_COND_PREFIX}_${layer.id}`,
|
||||
prompt: layer.positivePrompt,
|
||||
prompt: layer.textPrompt.positive,
|
||||
};
|
||||
graph.nodes[regionalPositiveCondNode.id] = regionalPositiveCondNode;
|
||||
|
||||
@ -162,19 +169,19 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
||||
}
|
||||
}
|
||||
|
||||
if (layer.negativePrompt) {
|
||||
if (layer.textPrompt?.negative) {
|
||||
// The main negative conditioning node
|
||||
const regionalNegativeCondNode: S['SDXLCompelPromptInvocation'] | S['CompelInvocation'] = isSDXL
|
||||
? {
|
||||
type: 'sdxl_compel_prompt',
|
||||
id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`,
|
||||
prompt: layer.negativePrompt,
|
||||
style: layer.negativePrompt,
|
||||
prompt: layer.textPrompt.negative,
|
||||
style: layer.textPrompt.negative,
|
||||
}
|
||||
: {
|
||||
type: 'compel',
|
||||
id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`,
|
||||
prompt: layer.negativePrompt,
|
||||
prompt: layer.textPrompt.negative,
|
||||
};
|
||||
graph.nodes[regionalNegativeCondNode.id] = regionalNegativeCondNode;
|
||||
|
||||
@ -202,7 +209,7 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
||||
}
|
||||
|
||||
// If we are using the "invert" auto-negative setting, we need to add an additional negative conditioning node
|
||||
if (layer.autoNegative === 'invert' && layer.positivePrompt) {
|
||||
if (layer.autoNegative === 'invert' && layer.textPrompt?.positive) {
|
||||
// We re-use the mask image, but invert it when converting to tensor
|
||||
const invertTensorMaskNode: S['InvertTensorMaskInvocation'] = {
|
||||
id: `${PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX}_${layer.id}`,
|
||||
@ -228,13 +235,13 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
||||
? {
|
||||
type: 'sdxl_compel_prompt',
|
||||
id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`,
|
||||
prompt: layer.positivePrompt,
|
||||
style: layer.positivePrompt,
|
||||
prompt: layer.textPrompt.positive,
|
||||
style: layer.textPrompt.positive,
|
||||
}
|
||||
: {
|
||||
type: 'compel',
|
||||
id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`,
|
||||
prompt: layer.positivePrompt,
|
||||
prompt: layer.textPrompt.positive,
|
||||
};
|
||||
graph.nodes[regionalPositiveCondInvertedNode.id] = regionalPositiveCondInvertedNode;
|
||||
// Connect the inverted mask to the conditioning
|
||||
|
@ -8,7 +8,7 @@ export const AddLayerButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(layerAdded('regionalPromptLayer'));
|
||||
dispatch(layerAdded('vector_mask_layer'));
|
||||
}, [dispatch]);
|
||||
|
||||
return <Button onClick={onClick}>{t('regionalPrompts.addLayer')}</Button>;
|
||||
|
@ -1,19 +1,19 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
globalMaskLayerOpacityChanged,
|
||||
initialRegionalPromptsState,
|
||||
promptLayerOpacityChanged,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const PromptLayerOpacity = memo(() => {
|
||||
export const GlobalMaskLayerOpacity = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const promptLayerOpacity = useAppSelector((s) => s.regionalPrompts.present.promptLayerOpacity);
|
||||
const globalMaskLayerOpacity = useAppSelector((s) => s.regionalPrompts.present.globalMaskLayerOpacity);
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(promptLayerOpacityChanged(v));
|
||||
dispatch(globalMaskLayerOpacityChanged(v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@ -24,20 +24,20 @@ export const PromptLayerOpacity = memo(() => {
|
||||
min={0.25}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={promptLayerOpacity}
|
||||
defaultValue={initialRegionalPromptsState.promptLayerOpacity}
|
||||
value={globalMaskLayerOpacity}
|
||||
defaultValue={initialRegionalPromptsState.globalMaskLayerOpacity}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<CompositeNumberInput
|
||||
min={0.25}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={promptLayerOpacity}
|
||||
defaultValue={initialRegionalPromptsState.promptLayerOpacity}
|
||||
value={globalMaskLayerOpacity}
|
||||
defaultValue={initialRegionalPromptsState.globalMaskLayerOpacity}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
PromptLayerOpacity.displayName = 'PromptLayerOpacity';
|
||||
GlobalMaskLayerOpacity.displayName = 'GlobalMaskLayerOpacity';
|
@ -1,6 +1,6 @@
|
||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { layerDeleted, rpLayerReset } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { layerDeleted, layerReset } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
@ -14,7 +14,7 @@ export const RPLayerActionsButtonGroup = memo(({ layerId }: Props) => {
|
||||
dispatch(layerDeleted(layerId));
|
||||
}, [dispatch, layerId]);
|
||||
const resetLayer = useCallback(() => {
|
||||
dispatch(rpLayerReset(layerId));
|
||||
dispatch(layerReset(layerId));
|
||||
}, [dispatch, layerId]);
|
||||
return (
|
||||
<ButtonGroup isAttached={false}>
|
||||
|
@ -4,8 +4,8 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { isParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
||||
import {
|
||||
isRPLayer,
|
||||
rpLayerAutoNegativeChanged,
|
||||
isVectorMaskLayer,
|
||||
maskLayerAutoNegativeChanged,
|
||||
selectRegionalPromptsSlice,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@ -26,7 +26,7 @@ const useAutoNegative = (layerId: string) => {
|
||||
() =>
|
||||
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isRPLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return layer.autoNegative;
|
||||
}),
|
||||
[layerId]
|
||||
@ -45,7 +45,7 @@ export const RPLayerAutoNegativeCombobox = memo(({ layerId }: Props) => {
|
||||
if (!isParameterAutoNegative(v?.value)) {
|
||||
return;
|
||||
}
|
||||
dispatch(rpLayerAutoNegativeChanged({ layerId, autoNegative: v.value }));
|
||||
dispatch(maskLayerAutoNegativeChanged({ layerId, autoNegative: v.value }));
|
||||
},
|
||||
[dispatch, layerId]
|
||||
);
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||
import {
|
||||
isRPLayer,
|
||||
rpLayerColorChanged,
|
||||
isVectorMaskLayer,
|
||||
maskLayerPreviewColorChanged,
|
||||
selectRegionalPromptsSlice,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import type { RgbColor } from 'react-colorful';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyedropperBold } from 'react-icons/pi';
|
||||
import { assert } from 'tsafe';
|
||||
@ -23,16 +23,16 @@ export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
||||
() =>
|
||||
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isRPLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return layer.color;
|
||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an vector mask layer`);
|
||||
return layer.previewColor;
|
||||
}),
|
||||
[layerId]
|
||||
);
|
||||
const color = useAppSelector(selectColor);
|
||||
const dispatch = useAppDispatch();
|
||||
const onColorChange = useCallback(
|
||||
(color: RgbColor) => {
|
||||
dispatch(rpLayerColorChanged({ layerId, color }));
|
||||
(color: RgbaColor) => {
|
||||
dispatch(maskLayerPreviewColorChanged({ layerId, color }));
|
||||
},
|
||||
[dispatch, layerId]
|
||||
);
|
||||
@ -49,7 +49,7 @@ export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody minH={64}>
|
||||
<RgbColorPicker color={color} onChange={onColorChange} withNumberInput />
|
||||
<IAIColorPicker color={color} onChange={onColorChange} withNumberInput />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
@ -8,7 +8,7 @@ import { RPLayerMenu } from 'features/regionalPrompts/components/RPLayerMenu';
|
||||
import { RPLayerNegativePrompt } from 'features/regionalPrompts/components/RPLayerNegativePrompt';
|
||||
import { RPLayerPositivePrompt } from 'features/regionalPrompts/components/RPLayerPositivePrompt';
|
||||
import { RPLayerVisibilityToggle } from 'features/regionalPrompts/components/RPLayerVisibilityToggle';
|
||||
import { isRPLayer, rpLayerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { isVectorMaskLayer, layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@ -21,24 +21,32 @@ export const RPLayerListItem = memo(({ layerId }: Props) => {
|
||||
const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId);
|
||||
const color = useAppSelector((s) => {
|
||||
const layer = s.regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isRPLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return rgbColorToString(layer.color);
|
||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return rgbColorToString(layer.previewColor);
|
||||
});
|
||||
const hasTextPrompt = useAppSelector((s) => {
|
||||
const layer = s.regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return layer.textPrompt !== null;
|
||||
});
|
||||
const onClickCapture = useCallback(() => {
|
||||
// Must be capture so that the layer is selected before deleting/resetting/etc
|
||||
dispatch(rpLayerSelected(layerId));
|
||||
dispatch(layerSelected(layerId));
|
||||
}, [dispatch, layerId]);
|
||||
return (
|
||||
<Flex
|
||||
gap={2}
|
||||
onClickCapture={onClickCapture}
|
||||
bg={color}
|
||||
px={2}
|
||||
borderRadius="base"
|
||||
p="1px"
|
||||
ps={2}
|
||||
borderTop="1px"
|
||||
borderBottom="1px"
|
||||
borderColor="base.800"
|
||||
opacity={selectedLayerId === layerId ? 1 : 0.5}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Flex flexDir="column" gap={2} w="full" bg="base.850" borderRadius="base" p={2}>
|
||||
<Flex flexDir="column" gap={2} w="full" bg="base.850" p={2}>
|
||||
<Flex gap={2} alignItems="center">
|
||||
<RPLayerMenu layerId={layerId} />
|
||||
<RPLayerColorPicker layerId={layerId} />
|
||||
@ -47,8 +55,8 @@ export const RPLayerListItem = memo(({ layerId }: Props) => {
|
||||
<RPLayerAutoNegativeCombobox layerId={layerId} />
|
||||
<RPLayerActionsButtonGroup layerId={layerId} />
|
||||
</Flex>
|
||||
<RPLayerPositivePrompt layerId={layerId} />
|
||||
<RPLayerNegativePrompt layerId={layerId} />
|
||||
{hasTextPrompt && <RPLayerPositivePrompt layerId={layerId} />}
|
||||
{hasTextPrompt && <RPLayerNegativePrompt layerId={layerId} />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -2,13 +2,13 @@ import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
isRPLayer,
|
||||
isVectorMaskLayer,
|
||||
layerDeleted,
|
||||
layerMovedBackward,
|
||||
layerMovedForward,
|
||||
layerMovedToBack,
|
||||
layerMovedToFront,
|
||||
rpLayerReset,
|
||||
layerReset,
|
||||
selectRegionalPromptsSlice,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@ -33,7 +33,7 @@ export const RPLayerMenu = memo(({ layerId }: Props) => {
|
||||
() =>
|
||||
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isRPLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
const layerIndex = regionalPrompts.present.layers.findIndex((l) => l.id === layerId);
|
||||
const layerCount = regionalPrompts.present.layers.length;
|
||||
return {
|
||||
@ -59,7 +59,7 @@ export const RPLayerMenu = memo(({ layerId }: Props) => {
|
||||
dispatch(layerMovedToBack(layerId));
|
||||
}, [dispatch, layerId]);
|
||||
const resetLayer = useCallback(() => {
|
||||
dispatch(rpLayerReset(layerId));
|
||||
dispatch(layerReset(layerId));
|
||||
}, [dispatch, layerId]);
|
||||
const deleteLayer = useCallback(() => {
|
||||
dispatch(layerDeleted(layerId));
|
||||
|
@ -4,8 +4,8 @@ import { PromptOverlayButtonWrapper } from 'features/parameters/components/Promp
|
||||
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 { rpLayerNegativePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { useMaskLayerTextPrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
||||
import { maskLayerNegativePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@ -16,18 +16,18 @@ type Props = {
|
||||
};
|
||||
|
||||
export const RPLayerNegativePrompt = memo((props: Props) => {
|
||||
const prompt = useLayerNegativePrompt(props.layerId);
|
||||
const textPrompt = useMaskLayerTextPrompt(props.layerId);
|
||||
const dispatch = useAppDispatch();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const _onChange = useCallback(
|
||||
(v: string) => {
|
||||
dispatch(rpLayerNegativePromptChanged({ layerId: props.layerId, prompt: v }));
|
||||
dispatch(maskLayerNegativePromptChanged({ layerId: props.layerId, prompt: v }));
|
||||
},
|
||||
[dispatch, props.layerId]
|
||||
);
|
||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
||||
prompt,
|
||||
prompt: textPrompt.negative,
|
||||
textareaRef,
|
||||
onChange: _onChange,
|
||||
});
|
||||
@ -48,7 +48,7 @@ export const RPLayerNegativePrompt = memo((props: Props) => {
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
ref={textareaRef}
|
||||
value={prompt}
|
||||
value={textPrompt.negative}
|
||||
placeholder={t('parameters.negativePromptPlaceholder')}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
|
@ -4,8 +4,8 @@ import { PromptOverlayButtonWrapper } from 'features/parameters/components/Promp
|
||||
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 { rpLayerPositivePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { useMaskLayerTextPrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
||||
import { maskLayerPositivePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import type { HotkeyCallback } from 'react-hotkeys-hook';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@ -16,18 +16,18 @@ type Props = {
|
||||
};
|
||||
|
||||
export const RPLayerPositivePrompt = memo((props: Props) => {
|
||||
const prompt = useLayerPositivePrompt(props.layerId);
|
||||
const textPrompt = useMaskLayerTextPrompt(props.layerId);
|
||||
const dispatch = useAppDispatch();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const _onChange = useCallback(
|
||||
(v: string) => {
|
||||
dispatch(rpLayerPositivePromptChanged({ layerId: props.layerId, prompt: v }));
|
||||
dispatch(maskLayerPositivePromptChanged({ layerId: props.layerId, prompt: v }));
|
||||
},
|
||||
[dispatch, props.layerId]
|
||||
);
|
||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
||||
prompt,
|
||||
prompt: textPrompt.positive,
|
||||
textareaRef,
|
||||
onChange: _onChange,
|
||||
});
|
||||
@ -48,7 +48,7 @@ export const RPLayerPositivePrompt = memo((props: Props) => {
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
ref={textareaRef}
|
||||
value={prompt}
|
||||
value={textPrompt.positive}
|
||||
placeholder={t('parameters.positivePromptPlaceholder')}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
|
@ -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 { rpLayerIsVisibleToggled } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { layerVisibilityToggled } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold, PiEyeClosedBold } from 'react-icons/pi';
|
||||
@ -15,7 +15,7 @@ export const RPLayerVisibilityToggle = memo(({ layerId }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isVisible = useLayerIsVisible(layerId);
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(rpLayerIsVisibleToggled(layerId));
|
||||
dispatch(layerVisibilityToggled(layerId));
|
||||
}, [dispatch, layerId]);
|
||||
|
||||
return (
|
||||
|
@ -7,18 +7,18 @@ import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButt
|
||||
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
|
||||
import { DebugLayersButton } from 'features/regionalPrompts/components/DebugLayersButton';
|
||||
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
|
||||
import { PromptLayerOpacity } from 'features/regionalPrompts/components/PromptLayerOpacity';
|
||||
import { GlobalMaskLayerOpacity } from 'features/regionalPrompts/components/GlobalMaskLayerOpacity';
|
||||
import { RPEnabledSwitch } from 'features/regionalPrompts/components/RPEnabledSwitch';
|
||||
import { RPLayerListItem } from 'features/regionalPrompts/components/RPLayerListItem';
|
||||
import { StageComponent } from 'features/regionalPrompts/components/StageComponent';
|
||||
import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser';
|
||||
import { UndoRedoButtonGroup } from 'features/regionalPrompts/components/UndoRedoButtonGroup';
|
||||
import { isRPLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { isVectorMaskLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectRPLayerIdsReversed = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) =>
|
||||
regionalPrompts.present.layers
|
||||
.filter(isRPLayer)
|
||||
.filter(isVectorMaskLayer)
|
||||
.map((l) => l.id)
|
||||
.reverse()
|
||||
);
|
||||
@ -38,7 +38,7 @@ export const RegionalPromptsEditor = memo(() => {
|
||||
</Flex>
|
||||
<RPEnabledSwitch />
|
||||
<BrushSize />
|
||||
<PromptLayerOpacity />
|
||||
<GlobalMaskLayerOpacity />
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
{rpLayerIdsReversed.map((id) => (
|
||||
|
@ -7,13 +7,13 @@ import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
||||
import {
|
||||
$cursorPosition,
|
||||
$tool,
|
||||
isRPLayer,
|
||||
rpLayerBboxChanged,
|
||||
rpLayerSelected,
|
||||
rpLayerTranslated,
|
||||
isVectorMaskLayer,
|
||||
layerBboxChanged,
|
||||
layerSelected,
|
||||
layerTranslated,
|
||||
selectRegionalPromptsSlice,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { renderBbox, renderBrushPreview, renderLayers } from 'features/regionalPrompts/util/renderers';
|
||||
import { renderBbox, renderLayers,renderToolPreview } from 'features/regionalPrompts/util/renderers';
|
||||
import Konva from 'konva';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { atom } from 'nanostores';
|
||||
@ -27,8 +27,8 @@ const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSli
|
||||
if (!layer) {
|
||||
return null;
|
||||
}
|
||||
assert(isRPLayer(layer), `Layer ${regionalPrompts.present.selectedLayerId} is not an RP layer`);
|
||||
return layer.color;
|
||||
assert(isVectorMaskLayer(layer), `Layer ${regionalPrompts.present.selectedLayerId} is not an RP layer`);
|
||||
return layer.previewColor;
|
||||
});
|
||||
|
||||
const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElement | null) => {
|
||||
@ -44,21 +44,21 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
|
||||
const onLayerPosChanged = useCallback(
|
||||
(layerId: string, x: number, y: number) => {
|
||||
dispatch(rpLayerTranslated({ layerId, x, y }));
|
||||
dispatch(layerTranslated({ layerId, x, y }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onBboxChanged = useCallback(
|
||||
(layerId: string, bbox: IRect | null) => {
|
||||
dispatch(rpLayerBboxChanged({ layerId, bbox }));
|
||||
dispatch(layerBboxChanged({ layerId, bbox }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onBboxMouseDown = useCallback(
|
||||
(layerId: string) => {
|
||||
dispatch(rpLayerSelected(layerId));
|
||||
dispatch(layerSelected(layerId));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@ -130,7 +130,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
renderBrushPreview(stage, tool, selectedLayerIdColor, cursorPosition, state.brushSize);
|
||||
renderToolPreview(stage, tool, selectedLayerIdColor, cursorPosition, state.brushSize);
|
||||
}, [stage, tool, cursorPosition, state.brushSize, selectedLayerIdColor]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -138,8 +138,8 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
renderLayers(stage, state.layers, state.selectedLayerId, state.promptLayerOpacity, tool, onLayerPosChanged);
|
||||
}, [onLayerPosChanged, stage, state.layers, state.promptLayerOpacity, tool, state.selectedLayerId]);
|
||||
renderLayers(stage, state.layers, tool, onLayerPosChanged);
|
||||
}, [stage, state.layers, tool, onLayerPosChanged]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Rendering bbox');
|
||||
|
@ -1,35 +1,22 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { isRPLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { isVectorMaskLayer, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useLayerPositivePrompt = (layerId: string) => {
|
||||
export const useMaskLayerTextPrompt = (layerId: string) => {
|
||||
const selectLayer = useMemo(
|
||||
() =>
|
||||
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isRPLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return layer.positivePrompt;
|
||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
assert(layer.textPrompt !== null, `Layer ${layerId} does not have a text prompt`);
|
||||
return layer.textPrompt;
|
||||
}),
|
||||
[layerId]
|
||||
);
|
||||
const prompt = useAppSelector(selectLayer);
|
||||
return prompt;
|
||||
};
|
||||
|
||||
export const useLayerNegativePrompt = (layerId: string) => {
|
||||
const selectLayer = useMemo(
|
||||
() =>
|
||||
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isRPLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return layer.negativePrompt;
|
||||
}),
|
||||
[layerId]
|
||||
);
|
||||
const prompt = useAppSelector(selectLayer);
|
||||
return prompt;
|
||||
const textPrompt = useAppSelector(selectLayer);
|
||||
return textPrompt;
|
||||
};
|
||||
|
||||
export const useLayerIsVisible = (layerId: string) => {
|
||||
@ -37,7 +24,7 @@ export const useLayerIsVisible = (layerId: string) => {
|
||||
() =>
|
||||
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||
assert(isRPLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||
return layer.isVisible;
|
||||
}),
|
||||
[layerId]
|
||||
|
@ -5,8 +5,8 @@ import {
|
||||
$isMouseDown,
|
||||
$isMouseOver,
|
||||
$tool,
|
||||
rpLayerLineAdded,
|
||||
rpLayerPointsAdded,
|
||||
lineAdded,
|
||||
pointsAddedToLastLine,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
@ -64,7 +64,7 @@ export const useMouseEvents = () => {
|
||||
// const tool = getTool();
|
||||
if (tool === 'brush' || tool === 'eraser') {
|
||||
dispatch(
|
||||
rpLayerLineAdded({
|
||||
lineAdded({
|
||||
layerId: selectedLayerId,
|
||||
points: [Math.floor(pos.x), Math.floor(pos.y), Math.floor(pos.x), Math.floor(pos.y)],
|
||||
tool,
|
||||
@ -101,7 +101,7 @@ export const useMouseEvents = () => {
|
||||
}
|
||||
// const tool = getTool();
|
||||
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
|
||||
dispatch(rpLayerPointsAdded({ layerId: selectedLayerId, point: [Math.floor(pos.x), Math.floor(pos.y)] }));
|
||||
dispatch(pointsAddedToLastLine({ layerId: selectedLayerId, point: [Math.floor(pos.x), Math.floor(pos.y)] }));
|
||||
}
|
||||
},
|
||||
[dispatch, selectedLayerId, tool]
|
||||
@ -140,7 +140,7 @@ export const useMouseEvents = () => {
|
||||
}
|
||||
if (tool === 'brush' || tool === 'eraser') {
|
||||
dispatch(
|
||||
rpLayerLineAdded({
|
||||
lineAdded({
|
||||
layerId: selectedLayerId,
|
||||
points: [Math.floor(pos.x), Math.floor(pos.y), Math.floor(pos.x), Math.floor(pos.y)],
|
||||
tool,
|
||||
|
@ -6,7 +6,7 @@ import type { ParameterAutoNegative } from 'features/parameters/types/parameterS
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import type { RgbColor } from 'react-colorful';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import type { UndoableOptions } from 'redux-undo';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@ -15,63 +15,63 @@ type DrawingTool = 'brush' | 'eraser';
|
||||
|
||||
export type RPTool = DrawingTool | 'move';
|
||||
|
||||
type LayerObjectBase = {
|
||||
type VectorMaskLine = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type ImageObject = LayerObjectBase & {
|
||||
kind: 'image';
|
||||
imageName: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type LineObject = LayerObjectBase & {
|
||||
kind: 'line';
|
||||
kind: 'vector_mask_line';
|
||||
tool: DrawingTool;
|
||||
strokeWidth: number;
|
||||
points: number[];
|
||||
};
|
||||
|
||||
type FillRectObject = LayerObjectBase & {
|
||||
kind: 'fillRect';
|
||||
type VectorMaskRect = {
|
||||
id: string;
|
||||
kind: 'vector_mask_rect';
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type LayerObject = ImageObject | LineObject | FillRectObject;
|
||||
type TextPrompt = {
|
||||
positive: string;
|
||||
negative: string;
|
||||
};
|
||||
|
||||
type ImagePrompt = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
type LayerBase = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type RegionalPromptLayer = LayerBase & {
|
||||
isVisible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
bbox: IRect | null;
|
||||
bboxNeedsUpdate: boolean;
|
||||
hasEraserStrokes: boolean;
|
||||
kind: 'regionalPromptLayer';
|
||||
objects: LayerObject[];
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
color: RgbColor;
|
||||
autoNegative: ParameterAutoNegative;
|
||||
isVisible: boolean;
|
||||
};
|
||||
|
||||
export type Layer = RegionalPromptLayer;
|
||||
type MaskLayerBase = LayerBase & {
|
||||
textPrompt: TextPrompt | null; // Up to one text prompt per mask
|
||||
imagePrompts: ImagePrompt[]; // Any number of image prompts
|
||||
previewColor: RgbaColor;
|
||||
autoNegative: ParameterAutoNegative;
|
||||
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object
|
||||
};
|
||||
|
||||
export type VectorMaskLayer = MaskLayerBase & {
|
||||
kind: 'vector_mask_layer';
|
||||
objects: (VectorMaskLine | VectorMaskRect)[];
|
||||
};
|
||||
|
||||
export type Layer = VectorMaskLayer;
|
||||
|
||||
type RegionalPromptsState = {
|
||||
_version: 1;
|
||||
selectedLayerId: string | null;
|
||||
layers: Layer[];
|
||||
brushSize: number;
|
||||
promptLayerOpacity: number;
|
||||
brushColor: RgbaColor;
|
||||
globalMaskLayerOpacity: number;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
|
||||
@ -79,13 +79,14 @@ export const initialRegionalPromptsState: RegionalPromptsState = {
|
||||
_version: 1,
|
||||
selectedLayerId: null,
|
||||
brushSize: 100,
|
||||
brushColor: { r: 255, g: 0, b: 0, a: 1 },
|
||||
layers: [],
|
||||
promptLayerOpacity: 0.5, // This currently doesn't work
|
||||
globalMaskLayerOpacity: 0.5, // This currently doesn't work
|
||||
isEnabled: false,
|
||||
};
|
||||
|
||||
const isLine = (obj: LayerObject): obj is LineObject => obj.kind === 'line';
|
||||
export const isRPLayer = (layer?: Layer): layer is RegionalPromptLayer => layer?.kind === 'regionalPromptLayer';
|
||||
const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.kind === 'vector_mask_line';
|
||||
export const isVectorMaskLayer = (layer?: Layer): layer is VectorMaskLayer => layer?.kind === 'vector_mask_layer';
|
||||
|
||||
export const regionalPromptsSlice = createSlice({
|
||||
name: 'regionalPrompts',
|
||||
@ -94,23 +95,27 @@ export const regionalPromptsSlice = createSlice({
|
||||
//#region Meta Layer
|
||||
layerAdded: {
|
||||
reducer: (state, action: PayloadAction<Layer['kind'], string, { uuid: string }>) => {
|
||||
if (action.payload === 'regionalPromptLayer') {
|
||||
const lastColor = state.layers[state.layers.length - 1]?.color;
|
||||
const kind = action.payload;
|
||||
if (action.payload === 'vector_mask_layer') {
|
||||
const lastColor = state.layers[state.layers.length - 1]?.previewColor;
|
||||
const color = LayerColors.next(lastColor);
|
||||
const layer: RegionalPromptLayer = {
|
||||
id: getRPLayerId(action.meta.uuid),
|
||||
const layer: VectorMaskLayer = {
|
||||
id: getVectorMaskLayerId(action.meta.uuid),
|
||||
kind,
|
||||
isVisible: true,
|
||||
bbox: null,
|
||||
kind: action.payload,
|
||||
positivePrompt: '',
|
||||
negativePrompt: '',
|
||||
bboxNeedsUpdate: false,
|
||||
objects: [],
|
||||
color,
|
||||
previewColor: color,
|
||||
x: 0,
|
||||
y: 0,
|
||||
autoNegative: 'off',
|
||||
bboxNeedsUpdate: false,
|
||||
hasEraserStrokes: false,
|
||||
needsPixelBbox: false,
|
||||
textPrompt: {
|
||||
positive: '',
|
||||
negative: '',
|
||||
},
|
||||
imagePrompts: [],
|
||||
};
|
||||
state.layers.push(layer);
|
||||
state.selectedLayerId = layer.id;
|
||||
@ -119,10 +124,52 @@ export const regionalPromptsSlice = createSlice({
|
||||
},
|
||||
prepare: (payload: Layer['kind']) => ({ payload, meta: { uuid: uuidv4() } }),
|
||||
},
|
||||
layerSelected: (state, action: PayloadAction<string>) => {
|
||||
const layer = state.layers.find((l) => l.id === action.payload);
|
||||
if (layer) {
|
||||
state.selectedLayerId = layer.id;
|
||||
}
|
||||
},
|
||||
layerVisibilityToggled: (state, action: PayloadAction<string>) => {
|
||||
const layer = state.layers.find((l) => l.id === action.payload);
|
||||
if (layer) {
|
||||
layer.isVisible = !layer.isVisible;
|
||||
}
|
||||
},
|
||||
layerTranslated: (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) {
|
||||
layer.x = x;
|
||||
layer.y = y;
|
||||
}
|
||||
},
|
||||
layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => {
|
||||
const { layerId, bbox } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (layer) {
|
||||
layer.bbox = bbox;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
}
|
||||
},
|
||||
layerReset: (state, action: PayloadAction<string>) => {
|
||||
const layer = state.layers.find((l) => l.id === action.payload);
|
||||
if (layer) {
|
||||
layer.objects = [];
|
||||
layer.bbox = null;
|
||||
layer.isVisible = true;
|
||||
layer.needsPixelBbox = false;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
}
|
||||
},
|
||||
layerDeleted: (state, action: PayloadAction<string>) => {
|
||||
state.layers = state.layers.filter((l) => l.id !== action.payload);
|
||||
state.selectedLayerId = state.layers[0]?.id ?? null;
|
||||
},
|
||||
allLayersDeleted: (state) => {
|
||||
state.layers = [];
|
||||
state.selectedLayerId = null;
|
||||
},
|
||||
layerMovedForward: (state, action: PayloadAction<string>) => {
|
||||
const cb = (l: Layer) => l.id === action.payload;
|
||||
moveForward(state.layers, cb);
|
||||
@ -143,70 +190,28 @@ export const regionalPromptsSlice = createSlice({
|
||||
},
|
||||
//#endregion
|
||||
//#region RP Layers
|
||||
rpLayerSelected: (state, action: PayloadAction<string>) => {
|
||||
const layer = state.layers.find((l) => l.id === action.payload);
|
||||
if (isRPLayer(layer)) {
|
||||
state.selectedLayerId = layer.id;
|
||||
}
|
||||
},
|
||||
rpLayerIsVisibleToggled: (state, action: PayloadAction<string>) => {
|
||||
const layer = state.layers.find((l) => l.id === action.payload);
|
||||
if (isRPLayer(layer)) {
|
||||
layer.isVisible = !layer.isVisible;
|
||||
}
|
||||
},
|
||||
rpLayerReset: (state, action: PayloadAction<string>) => {
|
||||
const layer = state.layers.find((l) => l.id === action.payload);
|
||||
if (isRPLayer(layer)) {
|
||||
layer.objects = [];
|
||||
layer.bbox = null;
|
||||
layer.isVisible = true;
|
||||
layer.hasEraserStrokes = false;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
}
|
||||
},
|
||||
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 (isRPLayer(layer)) {
|
||||
layer.x = x;
|
||||
layer.y = y;
|
||||
}
|
||||
},
|
||||
rpLayerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => {
|
||||
const { layerId, bbox } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
layer.bbox = bbox;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
}
|
||||
},
|
||||
allLayersDeleted: (state) => {
|
||||
state.layers = [];
|
||||
state.selectedLayerId = null;
|
||||
},
|
||||
rpLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
||||
maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
||||
const { layerId, prompt } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
layer.positivePrompt = prompt;
|
||||
if (layer && layer.textPrompt) {
|
||||
layer.textPrompt.positive = prompt;
|
||||
}
|
||||
},
|
||||
rpLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
||||
maskLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
||||
const { layerId, prompt } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
layer.negativePrompt = prompt;
|
||||
if (layer && layer.textPrompt) {
|
||||
layer.textPrompt.negative = prompt;
|
||||
}
|
||||
},
|
||||
rpLayerColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
|
||||
maskLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbaColor }>) => {
|
||||
const { layerId, color } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
layer.color = color;
|
||||
if (layer) {
|
||||
layer.previewColor = color;
|
||||
}
|
||||
},
|
||||
rpLayerLineAdded: {
|
||||
lineAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
@ -217,20 +222,20 @@ export const regionalPromptsSlice = createSlice({
|
||||
) => {
|
||||
const { layerId, points, tool } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
const lineId = getRPLayerLineId(layer.id, action.meta.uuid);
|
||||
if (layer) {
|
||||
const lineId = getVectorMaskLayerLineId(layer.id, action.meta.uuid);
|
||||
layer.objects.push({
|
||||
kind: 'line',
|
||||
kind: 'vector_mask_line',
|
||||
tool: tool,
|
||||
id: lineId,
|
||||
// Points must be offset by the layer's x and y coordinates
|
||||
// TODO: Handle this in the event listener
|
||||
// TODO: Handle this in the event listener?
|
||||
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
||||
strokeWidth: state.brushSize,
|
||||
});
|
||||
layer.bboxNeedsUpdate = true;
|
||||
if (!layer.hasEraserStrokes && tool === 'eraser') {
|
||||
layer.hasEraserStrokes = true;
|
||||
if (!layer.needsPixelBbox && tool === 'eraser') {
|
||||
layer.needsPixelBbox = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -239,10 +244,10 @@ export const regionalPromptsSlice = createSlice({
|
||||
meta: { uuid: uuidv4() },
|
||||
}),
|
||||
},
|
||||
rpLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => {
|
||||
pointsAddedToLastLine: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => {
|
||||
const { layerId, point } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
if (layer) {
|
||||
const lastLine = layer.objects.findLast(isLine);
|
||||
if (!lastLine) {
|
||||
return;
|
||||
@ -253,13 +258,13 @@ export const regionalPromptsSlice = createSlice({
|
||||
layer.bboxNeedsUpdate = true;
|
||||
}
|
||||
},
|
||||
rpLayerAutoNegativeChanged: (
|
||||
maskLayerAutoNegativeChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
|
||||
) => {
|
||||
const { layerId, autoNegative } = action.payload;
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
if (layer) {
|
||||
layer.autoNegative = autoNegative;
|
||||
}
|
||||
},
|
||||
@ -268,8 +273,11 @@ export const regionalPromptsSlice = createSlice({
|
||||
brushSizeChanged: (state, action: PayloadAction<number>) => {
|
||||
state.brushSize = action.payload;
|
||||
},
|
||||
promptLayerOpacityChanged: (state, action: PayloadAction<number>) => {
|
||||
state.promptLayerOpacity = action.payload;
|
||||
globalMaskLayerOpacityChanged: (state, action: PayloadAction<number>) => {
|
||||
state.globalMaskLayerOpacity = action.payload;
|
||||
for (const layer of state.layers) {
|
||||
layer.previewColor.a = action.payload;
|
||||
}
|
||||
},
|
||||
isEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isEnabled = action.payload;
|
||||
@ -282,22 +290,22 @@ export const regionalPromptsSlice = createSlice({
|
||||
* This class is used to cycle through a set of colors for the prompt region layers.
|
||||
*/
|
||||
class LayerColors {
|
||||
static COLORS: RgbColor[] = [
|
||||
{ r: 123, g: 159, b: 237 }, // rgb(123, 159, 237)
|
||||
{ r: 106, g: 222, b: 106 }, // rgb(106, 222, 106)
|
||||
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
|
||||
{ r: 233, g: 137, b: 81 }, // rgb(233, 137, 81)
|
||||
{ r: 229, g: 96, b: 96 }, // rgb(229, 96, 96)
|
||||
{ r: 226, g: 122, b: 210 }, // rgb(226, 122, 210)
|
||||
{ r: 167, g: 116, b: 234 }, // rgb(167, 116, 234)
|
||||
static COLORS: RgbaColor[] = [
|
||||
{ r: 123, g: 159, b: 237, a: 1 }, // rgb(123, 159, 237)
|
||||
{ r: 106, g: 222, b: 106, a: 1 }, // rgb(106, 222, 106)
|
||||
{ r: 250, g: 225, b: 80, a: 1 }, // rgb(250, 225, 80)
|
||||
{ r: 233, g: 137, b: 81, a: 1 }, // rgb(233, 137, 81)
|
||||
{ r: 229, g: 96, b: 96, a: 1 }, // rgb(229, 96, 96)
|
||||
{ r: 226, g: 122, b: 210, a: 1 }, // rgb(226, 122, 210)
|
||||
{ r: 167, g: 116, b: 234, a: 1 }, // rgb(167, 116, 234)
|
||||
];
|
||||
static i = this.COLORS.length - 1;
|
||||
/**
|
||||
* Get the next color in the sequence. If a known color is provided, the next color will be the one after it.
|
||||
*/
|
||||
static next(currentColor?: RgbColor): RgbColor {
|
||||
static next(currentColor?: RgbaColor): RgbaColor {
|
||||
if (currentColor) {
|
||||
const i = this.COLORS.findIndex((c) => isEqual(c, currentColor));
|
||||
const i = this.COLORS.findIndex((c) => isEqual(c, { ...currentColor, a: 1 }));
|
||||
if (i !== -1) {
|
||||
this.i = i;
|
||||
}
|
||||
@ -319,21 +327,21 @@ export const {
|
||||
layerMovedToFront,
|
||||
allLayersDeleted,
|
||||
// Regional Prompt layer actions
|
||||
rpLayerAutoNegativeChanged,
|
||||
rpLayerBboxChanged,
|
||||
rpLayerColorChanged,
|
||||
rpLayerIsVisibleToggled,
|
||||
rpLayerLineAdded,
|
||||
rpLayerNegativePromptChanged,
|
||||
rpLayerPointsAdded,
|
||||
rpLayerPositivePromptChanged,
|
||||
rpLayerReset,
|
||||
rpLayerSelected,
|
||||
rpLayerTranslated,
|
||||
maskLayerAutoNegativeChanged,
|
||||
layerBboxChanged,
|
||||
maskLayerPreviewColorChanged,
|
||||
layerVisibilityToggled,
|
||||
lineAdded,
|
||||
maskLayerNegativePromptChanged,
|
||||
pointsAddedToLastLine,
|
||||
maskLayerPositivePromptChanged,
|
||||
layerReset,
|
||||
layerSelected,
|
||||
layerTranslated,
|
||||
// General actions
|
||||
isEnabledChanged,
|
||||
brushSizeChanged,
|
||||
promptLayerOpacityChanged,
|
||||
globalMaskLayerOpacityChanged,
|
||||
} = regionalPromptsSlice.actions;
|
||||
|
||||
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;
|
||||
@ -349,22 +357,23 @@ export const $tool = atom<RPTool>('brush');
|
||||
export const $cursorPosition = atom<Vector2d | null>(null);
|
||||
|
||||
// IDs for singleton layers and objects
|
||||
export const BRUSH_PREVIEW_LAYER_ID = 'brushPreviewLayer';
|
||||
export const BRUSH_PREVIEW_FILL_ID = 'brushPreviewFill';
|
||||
export const BRUSH_PREVIEW_BORDER_INNER_ID = 'brushPreviewBorderInner';
|
||||
export const BRUSH_PREVIEW_BORDER_OUTER_ID = 'brushPreviewBorderOuter';
|
||||
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer';
|
||||
export const BRUSH_FILL_ID = 'brush_fill';
|
||||
export const BRUSH_BORDER_INNER_ID = 'brush_border_inner';
|
||||
export const BRUSH_BORDER_OUTER_ID = 'brush_border_outer';
|
||||
|
||||
// Names (aka classes) for Konva layers and objects
|
||||
export const REGIONAL_PROMPT_LAYER_NAME = 'regionalPromptLayer';
|
||||
export const REGIONAL_PROMPT_LAYER_LINE_NAME = 'regionalPromptLayerLine';
|
||||
export const REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME = 'regionalPromptLayerObjectGroup';
|
||||
export const REGIONAL_PROMPT_LAYER_BBOX_NAME = 'regionalPromptLayerBbox';
|
||||
export const VECTOR_MASK_LAYER_NAME = 'vector_mask_layer';
|
||||
export const VECTOR_MASK_LAYER_LINE_NAME = 'vector_mask_layer.line';
|
||||
export const VECTOR_MASK_LAYER_OBJECT_GROUP_NAME = 'vector_mask_layer.object_group';
|
||||
export const LAYER_BBOX_NAME = 'layer.bbox';
|
||||
|
||||
// Getters for non-singleton layer and object IDs
|
||||
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`;
|
||||
const getVectorMaskLayerId = (layerId: string) => `${VECTOR_MASK_LAYER_NAME}_${layerId}`;
|
||||
const getVectorMaskLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
||||
export const getVectorMaskLayerObjectGroupId = (layerId: string, groupId: string) =>
|
||||
`${layerId}.objectGroup_${groupId}`;
|
||||
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
||||
|
||||
export const regionalPromptsPersistConfig: PersistConfig<RegionalPromptsState> = {
|
||||
name: regionalPromptsSlice.name,
|
||||
@ -380,12 +389,12 @@ export const redoRegionalPrompts = createAction(`${regionalPromptsSlice.name}/re
|
||||
// These actions are _individually_ grouped together as single undoable actions
|
||||
const undoableGroupByMatcher = isAnyOf(
|
||||
brushSizeChanged,
|
||||
promptLayerOpacityChanged,
|
||||
globalMaskLayerOpacityChanged,
|
||||
isEnabledChanged,
|
||||
rpLayerPositivePromptChanged,
|
||||
rpLayerNegativePromptChanged,
|
||||
rpLayerTranslated,
|
||||
rpLayerColorChanged
|
||||
maskLayerPositivePromptChanged,
|
||||
maskLayerNegativePromptChanged,
|
||||
layerTranslated,
|
||||
maskLayerPreviewColorChanged
|
||||
);
|
||||
|
||||
const LINE_1 = 'LINE_1';
|
||||
@ -396,13 +405,13 @@ export const regionalPromptsUndoableConfig: UndoableOptions<RegionalPromptsState
|
||||
undoType: undoRegionalPrompts.type,
|
||||
redoType: redoRegionalPrompts.type,
|
||||
groupBy: (action, state, history) => {
|
||||
// Lines are started with `rpLayerLineAdded` and may have any number of subsequent `rpLayerPointsAdded` events.
|
||||
// Lines are started with `lineAdded` and may have any number of subsequent `pointsAddedToLastLine` 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)) {
|
||||
if (lineAdded.match(action)) {
|
||||
return history.group === LINE_1 ? LINE_2 : LINE_1;
|
||||
}
|
||||
if (rpLayerPointsAdded.match(action)) {
|
||||
if (pointsAddedToLastLine.match(action)) {
|
||||
if (history.group === LINE_1 || history.group === LINE_2) {
|
||||
return history.group;
|
||||
}
|
||||
@ -419,7 +428,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 (rpLayerBboxChanged.match(action)) {
|
||||
if (layerBboxChanged.match(action)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||
import { REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { VECTOR_MASK_LAYER_OBJECT_GROUP_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import Konva from 'konva';
|
||||
import type { Layer as KonvaLayerType } from 'konva/lib/Layer';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
@ -80,7 +80,7 @@ export const getKonvaLayerBbox = (layer: KonvaLayerType, preview: boolean = fals
|
||||
offscreenStage.add(layerClone);
|
||||
|
||||
for (const child of layerClone.getChildren()) {
|
||||
if (child.name() === REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME) {
|
||||
if (child.name() === VECTOR_MASK_LAYER_OBJECT_GROUP_NAME) {
|
||||
// We need to cache the group to ensure it composites out eraser strokes correctly
|
||||
child.cache();
|
||||
} else {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||
import { REGIONAL_PROMPT_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { VECTOR_MASK_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { renderLayers } from 'features/regionalPrompts/util/renderers';
|
||||
import Konva from 'konva';
|
||||
import { assert } from 'tsafe';
|
||||
@ -18,12 +18,11 @@ export const getRegionalPromptLayerBlobs = async (
|
||||
): Promise<Record<string, Blob>> => {
|
||||
const state = getStore().getState();
|
||||
const reduxLayers = state.regionalPrompts.present.layers;
|
||||
const selectedLayerIdId = state.regionalPrompts.present.selectedLayerId;
|
||||
const container = document.createElement('div');
|
||||
const stage = new Konva.Stage({ container, width: state.generation.width, height: state.generation.height });
|
||||
renderLayers(stage, reduxLayers, selectedLayerIdId, 1, 'brush');
|
||||
renderLayers(stage, reduxLayers, 'brush');
|
||||
|
||||
const konvaLayers = stage.find<Konva.Layer>(`.${REGIONAL_PROMPT_LAYER_NAME}`);
|
||||
const konvaLayers = stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`);
|
||||
const blobs: Record<string, Blob> = {};
|
||||
|
||||
// First remove all layers
|
||||
@ -51,7 +50,10 @@ export const getRegionalPromptLayerBlobs = async (
|
||||
if (preview) {
|
||||
const base64 = await blobToDataURL(blob);
|
||||
openBase64ImageInTab([
|
||||
{ base64, caption: `${reduxLayer.id}: ${reduxLayer.positivePrompt} / ${reduxLayer.negativePrompt}` },
|
||||
{
|
||||
base64,
|
||||
caption: `${reduxLayer.id}: ${reduxLayer.textPrompt?.positive} / ${reduxLayer.textPrompt?.negative}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
layer.remove();
|
||||
|
@ -1,20 +1,21 @@
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||
import { getScaledFlooredCursorPosition } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
||||
import type { Layer, RegionalPromptLayer, RPTool } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import type { Layer, RPTool, VectorMaskLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import {
|
||||
$isMouseOver,
|
||||
$tool,
|
||||
BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
BRUSH_PREVIEW_FILL_ID,
|
||||
BRUSH_PREVIEW_LAYER_ID,
|
||||
getPRLayerBboxId,
|
||||
getRPLayerObjectGroupId,
|
||||
REGIONAL_PROMPT_LAYER_BBOX_NAME,
|
||||
REGIONAL_PROMPT_LAYER_LINE_NAME,
|
||||
REGIONAL_PROMPT_LAYER_NAME,
|
||||
REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME,
|
||||
BRUSH_BORDER_INNER_ID,
|
||||
BRUSH_BORDER_OUTER_ID,
|
||||
BRUSH_FILL_ID,
|
||||
getLayerBboxId,
|
||||
getVectorMaskLayerObjectGroupId,
|
||||
isVectorMaskLayer,
|
||||
LAYER_BBOX_NAME,
|
||||
TOOL_PREVIEW_LAYER_ID,
|
||||
VECTOR_MASK_LAYER_LINE_NAME,
|
||||
VECTOR_MASK_LAYER_NAME,
|
||||
VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox';
|
||||
import Konva from 'konva';
|
||||
@ -26,11 +27,13 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
const BBOX_SELECTED_STROKE = 'rgba(78, 190, 255, 1)';
|
||||
const BBOX_NOT_SELECTED_STROKE = 'rgba(255, 255, 255, 0.353)';
|
||||
const BBOX_NOT_SELECTED_MOUSEOVER_STROKE = 'rgba(255, 255, 255, 0.661)';
|
||||
const BRUSH_PREVIEW_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
|
||||
const BRUSH_PREVIEW_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
|
||||
const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
|
||||
const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
|
||||
|
||||
const GET_CLIENT_RECT_CONFIG = { skipTransform: true };
|
||||
|
||||
const mapId = (object: { id: string }) => object.id;
|
||||
|
||||
const getIsSelected = (layerId?: string | null) => {
|
||||
if (!layerId) {
|
||||
return false;
|
||||
@ -46,37 +49,44 @@ const getIsSelected = (layerId?: string | null) => {
|
||||
* @param cursorPos The cursor position.
|
||||
* @param brushSize The brush size.
|
||||
*/
|
||||
export const renderBrushPreview = (
|
||||
export const renderToolPreview = (
|
||||
stage: Konva.Stage,
|
||||
tool: RPTool,
|
||||
color: RgbColor | null,
|
||||
cursorPos: Vector2d | null,
|
||||
brushSize: number
|
||||
) => {
|
||||
const layerCount = stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`).length;
|
||||
const layerCount = stage.find(`.${VECTOR_MASK_LAYER_NAME}`).length;
|
||||
// Update the stage's pointer style
|
||||
stage.container().style.cursor = tool === 'move' || layerCount === 0 ? 'default' : 'none';
|
||||
if (tool === 'move') {
|
||||
stage.container().style.cursor = 'default';
|
||||
} else if (layerCount === 0) {
|
||||
// We have no layers, so we should not render any tool
|
||||
stage.container().style.cursor = 'default';
|
||||
} else {
|
||||
stage.container().style.cursor = 'none';
|
||||
}
|
||||
|
||||
// Create the layer if it doesn't exist
|
||||
let layer = stage.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`);
|
||||
let layer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`);
|
||||
if (!layer) {
|
||||
// Initialize the brush preview layer & add to the stage
|
||||
layer = new Konva.Layer({ id: BRUSH_PREVIEW_LAYER_ID, visible: tool !== 'move', listening: false });
|
||||
layer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: tool !== 'move', listening: false });
|
||||
stage.add(layer);
|
||||
// The brush preview is hidden and shown as the mouse leaves and enters the stage
|
||||
stage.on('mousemove', (e) => {
|
||||
e.target
|
||||
.getStage()
|
||||
?.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)
|
||||
?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)
|
||||
?.visible($tool.get() !== 'move');
|
||||
});
|
||||
stage.on('mouseleave', (e) => {
|
||||
e.target.getStage()?.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)?.visible(false);
|
||||
e.target.getStage()?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false);
|
||||
});
|
||||
stage.on('mouseenter', (e) => {
|
||||
e.target
|
||||
.getStage()
|
||||
?.findOne<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)
|
||||
?.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)
|
||||
?.visible($tool.get() !== 'move');
|
||||
});
|
||||
}
|
||||
@ -95,10 +105,10 @@ export const renderBrushPreview = (
|
||||
}
|
||||
|
||||
// Create and/or update the fill circle
|
||||
let fill = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_FILL_ID}`);
|
||||
let fill = layer.findOne<Konva.Circle>(`#${BRUSH_FILL_ID}`);
|
||||
if (!fill) {
|
||||
fill = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_FILL_ID,
|
||||
id: BRUSH_FILL_ID,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
});
|
||||
@ -113,12 +123,12 @@ export const renderBrushPreview = (
|
||||
});
|
||||
|
||||
// Create and/or update the inner border of the brush preview
|
||||
let borderInner = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_BORDER_INNER_ID}`);
|
||||
let borderInner = layer.findOne<Konva.Circle>(`#${BRUSH_BORDER_INNER_ID}`);
|
||||
if (!borderInner) {
|
||||
borderInner = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_INNER_ID,
|
||||
id: BRUSH_BORDER_INNER_ID,
|
||||
listening: false,
|
||||
stroke: BRUSH_PREVIEW_BORDER_INNER_COLOR,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
@ -127,12 +137,12 @@ export const renderBrushPreview = (
|
||||
borderInner.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
||||
|
||||
// Create and/or update the outer border of the brush preview
|
||||
let borderOuter = layer.findOne<Konva.Circle>(`#${BRUSH_PREVIEW_BORDER_OUTER_ID}`);
|
||||
let borderOuter = layer.findOne<Konva.Circle>(`#${BRUSH_BORDER_OUTER_ID}`);
|
||||
if (!borderOuter) {
|
||||
borderOuter = new Konva.Circle({
|
||||
id: BRUSH_PREVIEW_BORDER_OUTER_ID,
|
||||
id: BRUSH_BORDER_OUTER_ID,
|
||||
listening: false,
|
||||
stroke: BRUSH_PREVIEW_BORDER_OUTER_COLOR,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
@ -145,22 +155,20 @@ export const renderBrushPreview = (
|
||||
});
|
||||
};
|
||||
|
||||
const renderRPLayer = (
|
||||
const renderVectorMaskLayer = (
|
||||
stage: Konva.Stage,
|
||||
rpLayer: RegionalPromptLayer,
|
||||
rpLayerIndex: number,
|
||||
selectedLayerIdId: string | null,
|
||||
vmLayer: VectorMaskLayer,
|
||||
vmLayerIndex: number,
|
||||
tool: RPTool,
|
||||
layerOpacity: number,
|
||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||
) => {
|
||||
let konvaLayer = stage.findOne<Konva.Layer>(`#${rpLayer.id}`);
|
||||
let konvaLayer = stage.findOne<Konva.Layer>(`#${vmLayer.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,
|
||||
id: vmLayer.id,
|
||||
name: VECTOR_MASK_LAYER_NAME,
|
||||
draggable: true,
|
||||
dragDistance: 0,
|
||||
});
|
||||
@ -168,7 +176,7 @@ const renderRPLayer = (
|
||||
// Create a `dragmove` listener for this layer
|
||||
if (onLayerPosChanged) {
|
||||
konvaLayer.on('dragend', function (e) {
|
||||
onLayerPosChanged(rpLayer.id, Math.floor(e.target.x()), Math.floor(e.target.y()));
|
||||
onLayerPosChanged(vmLayer.id, Math.floor(e.target.x()), Math.floor(e.target.y()));
|
||||
});
|
||||
}
|
||||
|
||||
@ -192,8 +200,8 @@ const renderRPLayer = (
|
||||
|
||||
// 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,
|
||||
id: getVectorMaskLayerObjectGroupId(vmLayer.id, uuidv4()),
|
||||
name: VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
||||
listening: false,
|
||||
});
|
||||
konvaLayer.add(konvaObjectGroup);
|
||||
@ -201,84 +209,92 @@ const renderRPLayer = (
|
||||
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<Konva.Layer>(`#${BRUSH_PREVIEW_LAYER_ID}`)?.moveToTop();
|
||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.moveToTop();
|
||||
}
|
||||
|
||||
// Update the layer's position and listening state
|
||||
konvaLayer.setAttrs({
|
||||
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
||||
x: Math.floor(rpLayer.x),
|
||||
y: Math.floor(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,
|
||||
x: Math.floor(vmLayer.x),
|
||||
y: Math.floor(vmLayer.y),
|
||||
// We have a konva layer for each redux layer, plus a brush preview layer, which should always be on top. We can
|
||||
// therefore use the index of the redux layer as the zIndex for konva layers. If more layers are added to the
|
||||
// stage, this may no longer be work.
|
||||
zIndex: vmLayerIndex,
|
||||
});
|
||||
|
||||
const color = rgbColorToString(rpLayer.color);
|
||||
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
||||
const rgbColor = rgbColorToString(vmLayer.previewColor);
|
||||
|
||||
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME}`);
|
||||
assert(konvaObjectGroup, `Object group not found for layer ${rpLayer.id}`);
|
||||
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${VECTOR_MASK_LAYER_OBJECT_GROUP_NAME}`);
|
||||
assert(konvaObjectGroup, `Object group not found for layer ${vmLayer.id}`);
|
||||
|
||||
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
|
||||
let groupNeedsCache = false;
|
||||
|
||||
if (konvaObjectGroup.opacity() !== layerOpacity) {
|
||||
konvaObjectGroup.opacity(layerOpacity);
|
||||
}
|
||||
|
||||
// Remove deleted objects
|
||||
const objectIds = rpLayer.objects.map(mapId);
|
||||
for (const objectNode of konvaLayer.find(`.${REGIONAL_PROMPT_LAYER_LINE_NAME}`)) {
|
||||
const objectIds = vmLayer.objects.map(mapId);
|
||||
for (const objectNode of konvaObjectGroup.find(`.${VECTOR_MASK_LAYER_LINE_NAME}`)) {
|
||||
if (!objectIds.includes(objectNode.id())) {
|
||||
objectNode.destroy();
|
||||
groupNeedsCache = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const reduxObject of rpLayer.objects) {
|
||||
// TODO: Handle rects, images, etc
|
||||
if (reduxObject.kind !== 'line') {
|
||||
continue;
|
||||
}
|
||||
for (const reduxObject of vmLayer.objects) {
|
||||
if (reduxObject.kind === 'vector_mask_line') {
|
||||
let vectorMaskLine = stage.findOne<Konva.Line>(`#${reduxObject.id}`);
|
||||
|
||||
let konvaObject = stage.findOne<Konva.Line>(`#${reduxObject.id}`);
|
||||
// Create the line if it doesn't exist
|
||||
if (!vectorMaskLine) {
|
||||
vectorMaskLine = new Konva.Line({
|
||||
id: reduxObject.id,
|
||||
key: reduxObject.id,
|
||||
name: VECTOR_MASK_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(vectorMaskLine);
|
||||
}
|
||||
|
||||
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);
|
||||
groupNeedsCache = true;
|
||||
}
|
||||
// Only update the color if it has changed.
|
||||
if (konvaObject.stroke() !== color) {
|
||||
konvaObject.stroke(color);
|
||||
groupNeedsCache = true;
|
||||
}
|
||||
// Only update layer visibility if it has changed.
|
||||
if (konvaLayer.visible() !== rpLayer.isVisible) {
|
||||
konvaLayer.visible(rpLayer.isVisible);
|
||||
groupNeedsCache = true;
|
||||
// Only update the points if they have changed. The point values are never mutated, they are only added to the
|
||||
// array, so checking the length is sufficient to determine if we need to re-cache.
|
||||
if (vectorMaskLine.points().length !== reduxObject.points.length) {
|
||||
vectorMaskLine.points(reduxObject.points);
|
||||
groupNeedsCache = true;
|
||||
}
|
||||
// Only update the color if it has changed.
|
||||
if (vectorMaskLine.stroke() !== rgbColor) {
|
||||
vectorMaskLine.stroke(rgbColor);
|
||||
groupNeedsCache = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groupNeedsCache) {
|
||||
konvaObjectGroup.cache();
|
||||
// Only update layer visibility if it has changed.
|
||||
if (konvaLayer.visible() !== vmLayer.isVisible) {
|
||||
konvaLayer.visible(vmLayer.isVisible);
|
||||
groupNeedsCache = true;
|
||||
}
|
||||
|
||||
if (konvaObjectGroup.children.length > 0) {
|
||||
// If we have objects, we need to cache the group to apply the layer opacity...
|
||||
if (groupNeedsCache) {
|
||||
// ...but only if we've done something that needs the cache.
|
||||
konvaObjectGroup.cache();
|
||||
}
|
||||
} else {
|
||||
// No children - clear the cache to reset the previous pixel data
|
||||
konvaObjectGroup.clearCache();
|
||||
}
|
||||
|
||||
// Updating group opacity does not require re-caching
|
||||
if (konvaObjectGroup.opacity() !== vmLayer.previewColor.a) {
|
||||
konvaObjectGroup.opacity(vmLayer.previewColor.a);
|
||||
}
|
||||
};
|
||||
|
||||
@ -286,7 +302,6 @@ const renderRPLayer = (
|
||||
* Renders the layers on the stage.
|
||||
* @param stage The konva stage to render on.
|
||||
* @param reduxLayers Array of the layers from the redux store.
|
||||
* @param selectedLayerIdId 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
|
||||
@ -294,15 +309,13 @@ const renderRPLayer = (
|
||||
export const renderLayers = (
|
||||
stage: Konva.Stage,
|
||||
reduxLayers: Layer[],
|
||||
selectedLayerIdId: string | null,
|
||||
layerOpacity: number,
|
||||
tool: RPTool,
|
||||
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}`)) {
|
||||
for (const konvaLayer of stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`)) {
|
||||
if (!reduxLayerIds.includes(konvaLayer.id())) {
|
||||
konvaLayer.destroy();
|
||||
}
|
||||
@ -311,8 +324,8 @@ export const renderLayers = (
|
||||
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, selectedLayerIdId, tool, layerOpacity, onLayerPosChanged);
|
||||
if (isVectorMaskLayer(reduxLayer)) {
|
||||
renderVectorMaskLayer(stage, reduxLayer, layerIndex, tool, onLayerPosChanged);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -335,7 +348,7 @@ export const renderBbox = (
|
||||
) => {
|
||||
// No selected layer or not using the move tool - nothing more to do here
|
||||
if (tool !== 'move') {
|
||||
for (const bboxRect of stage.find<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`)) {
|
||||
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
||||
bboxRect.visible(false);
|
||||
bboxRect.listening(false);
|
||||
}
|
||||
@ -351,7 +364,7 @@ export const renderBbox = (
|
||||
// We only need to recalculate the bbox if the layer has changed and it has objects
|
||||
if (reduxLayer.bboxNeedsUpdate && reduxLayer.objects.length) {
|
||||
// We only need to use the pixel-perfect bounding box if the layer has eraser strokes
|
||||
bbox = reduxLayer.hasEraserStrokes
|
||||
bbox = reduxLayer.needsPixelBbox
|
||||
? getKonvaLayerBbox(konvaLayer)
|
||||
: konvaLayer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
||||
|
||||
@ -363,11 +376,11 @@ export const renderBbox = (
|
||||
continue;
|
||||
}
|
||||
|
||||
let rect = konvaLayer.findOne<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`);
|
||||
let rect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`);
|
||||
if (!rect) {
|
||||
rect = new Konva.Rect({
|
||||
id: getPRLayerBboxId(reduxLayer.id),
|
||||
name: REGIONAL_PROMPT_LAYER_BBOX_NAME,
|
||||
id: getLayerBboxId(reduxLayer.id),
|
||||
name: LAYER_BBOX_NAME,
|
||||
strokeWidth: 1,
|
||||
});
|
||||
rect.on('mousedown', function () {
|
||||
|
@ -1,18 +1,30 @@
|
||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
|
||||
import { RegionalPromptsEditor } from 'features/regionalPrompts/components/RegionalPromptsEditor';
|
||||
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectValidLayerCount = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||
if (!regionalPrompts.present.isEnabled) {
|
||||
return 0;
|
||||
}
|
||||
const validLayers = regionalPrompts.present.layers
|
||||
.filter((l) => l.isVisible)
|
||||
.filter((l) => {
|
||||
const hasTextPrompt = l.textPrompt && (l.textPrompt.positive || l.textPrompt.negative);
|
||||
const hasAtLeastOneImagePrompt = l.imagePrompts.length > 0;
|
||||
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
||||
});
|
||||
|
||||
return validLayers.length;
|
||||
});
|
||||
|
||||
const TextToImageTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const noOfRPLayers = useAppSelector((s) => {
|
||||
if (!s.regionalPrompts.present.isEnabled) {
|
||||
return 0;
|
||||
}
|
||||
return s.regionalPrompts.present.layers.filter((l) => l.kind === 'regionalPromptLayer' && l.isVisible).length;
|
||||
});
|
||||
const validLayerCount = useAppSelector(selectValidLayerCount);
|
||||
return (
|
||||
<Box position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
|
||||
@ -20,7 +32,7 @@ const TextToImageTab = () => {
|
||||
<Tab>{t('common.viewer')}</Tab>
|
||||
<Tab>
|
||||
{t('regionalPrompts.regionalPrompts')}
|
||||
{noOfRPLayers > 0 ? ` (${noOfRPLayers})` : ''}
|
||||
{validLayerCount > 0 ? ` (${validLayerCount})` : ''}
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user