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