mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): regional prompts spray n pray
Trying a lot of different things as I iterated, so this is smooshed into one big commit... too hard to split it now. - Iterated on IP adapter handling and UI. Unfortunately there is an bug related to undo/redo. The IP adapter state is split across the `controlAdapters` slice and the `regionalPrompts` slice, but only the `regionalPrompts` slice supports undo/redo. If you delete the IP adapter and then undo/redo to a history state where it existed, you'll get an error. The fix is likely to merge the slices... Maybe there's a workaround. - Iterated on UI. I think the layers are OK now. - Removed ability to disable RP globally for now. It's enabled if you have enabled RP layers. - Many minor tweaks and fixes.
This commit is contained in:
parent
018845cda0
commit
6dcaf75b5f
@ -84,6 +84,8 @@
|
|||||||
"direction": "Direction",
|
"direction": "Direction",
|
||||||
"ipAdapter": "IP Adapter",
|
"ipAdapter": "IP Adapter",
|
||||||
"t2iAdapter": "T2I Adapter",
|
"t2iAdapter": "T2I Adapter",
|
||||||
|
"positivePrompt": "Positive Prompt",
|
||||||
|
"negativePrompt": "Negative Prompt",
|
||||||
"discordLabel": "Discord",
|
"discordLabel": "Discord",
|
||||||
"dontAskMeAgain": "Don't ask me again",
|
"dontAskMeAgain": "Don't ask me again",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@ -1518,11 +1520,16 @@
|
|||||||
"brushSize": "Brush Size",
|
"brushSize": "Brush Size",
|
||||||
"regionalPrompts": "Regional Prompts BETA",
|
"regionalPrompts": "Regional Prompts BETA",
|
||||||
"enableRegionalPrompts": "Enable $t(regionalPrompts.regionalPrompts)",
|
"enableRegionalPrompts": "Enable $t(regionalPrompts.regionalPrompts)",
|
||||||
"layerOpacity": "Layer Opacity",
|
"globalMaskOpacity": "Global Mask Opacity",
|
||||||
"autoNegative": "Auto Negative",
|
"autoNegative": "Auto Negative",
|
||||||
"toggleVisibility": "Toggle Layer Visibility",
|
"toggleVisibility": "Toggle Layer Visibility",
|
||||||
|
"deletePrompt": "Delete Prompt",
|
||||||
"resetRegion": "Reset Region",
|
"resetRegion": "Reset Region",
|
||||||
"debugLayers": "Debug Layers",
|
"debugLayers": "Debug Layers",
|
||||||
"rectangle": "Rectangle"
|
"rectangle": "Rectangle",
|
||||||
|
"maskPreviewColor": "Mask Preview Color",
|
||||||
|
"addPositivePrompt": "Add $t(common.positivePrompt)",
|
||||||
|
"addNegativePrompt": "Add $t(common.negativePrompt)",
|
||||||
|
"addIPAdapter": "Add $t(common.ipAdapter)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
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'] = '3.5rem';
|
||||||
|
|
||||||
|
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')[0]}</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')[0]}</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')[0]}</FormLabel>
|
||||||
|
<CompositeNumberInput
|
||||||
|
value={color.b}
|
||||||
|
onChange={handleChangeB}
|
||||||
|
min={0}
|
||||||
|
max={255}
|
||||||
|
step={1}
|
||||||
|
w={numberInputWidth}
|
||||||
|
defaultValue={255}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(RgbColorPicker);
|
@ -6,6 +6,7 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
|
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
|
||||||
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
|
import { maskLayerIPAdapterAdded } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { merge, uniq } from 'lodash-es';
|
import { merge, uniq } from 'lodash-es';
|
||||||
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
import { socketInvocationError } from 'services/events/actions';
|
import { socketInvocationError } from 'services/events/actions';
|
||||||
@ -382,6 +383,10 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
builder.addCase(socketInvocationError, (state) => {
|
builder.addCase(socketInvocationError, (state) => {
|
||||||
state.pendingControlImages = [];
|
state.pendingControlImages = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.addCase(maskLayerIPAdapterAdded, (state, action) => {
|
||||||
|
caAdapter.addOne(state, buildControlAdapter(action.meta.uuid, 'ip_adapter'));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,12 +19,14 @@ export const addIPAdapterToLinearGraph = async (
|
|||||||
graph: NonNullableGraph,
|
graph: NonNullableGraph,
|
||||||
baseNodeId: string
|
baseNodeId: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const validIPAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => {
|
const validIPAdapters = selectValidIPAdapters(state.controlAdapters)
|
||||||
const hasModel = Boolean(model);
|
.filter(({ model, controlImage, isEnabled }) => {
|
||||||
const doesBaseMatch = model?.base === state.generation.model?.base;
|
const hasModel = Boolean(model);
|
||||||
const hasControlImage = controlImage;
|
const doesBaseMatch = model?.base === state.generation.model?.base;
|
||||||
return isEnabled && hasModel && doesBaseMatch && hasControlImage;
|
const hasControlImage = controlImage;
|
||||||
});
|
return isEnabled && hasModel && doesBaseMatch && hasControlImage;
|
||||||
|
})
|
||||||
|
.filter((ca) => !state.regionalPrompts.present.layers.some((l) => l.ipAdapterIds.includes(ca.id)));
|
||||||
|
|
||||||
if (validIPAdapters.length) {
|
if (validIPAdapters.length) {
|
||||||
// Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect
|
// Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
|
import { selectAllIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import {
|
import {
|
||||||
|
IP_ADAPTER_COLLECT,
|
||||||
NEGATIVE_CONDITIONING,
|
NEGATIVE_CONDITIONING,
|
||||||
NEGATIVE_CONDITIONING_COLLECT,
|
NEGATIVE_CONDITIONING_COLLECT,
|
||||||
POSITIVE_CONDITIONING,
|
POSITIVE_CONDITIONING,
|
||||||
@ -13,9 +15,9 @@ import {
|
|||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { isVectorMaskLayer } 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, sumBy } from 'lodash-es';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { CollectInvocation, Edge, NonNullableGraph, S } from 'services/api/types';
|
import type { CollectInvocation, Edge, IPAdapterInvocation, NonNullableGraph, S } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => {
|
export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
@ -32,9 +34,9 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
|||||||
.filter((l) => l.isVisible)
|
.filter((l) => l.isVisible)
|
||||||
// Only layers with prompts get added to the graph
|
// Only layers with prompts get added to the graph
|
||||||
.filter((l) => {
|
.filter((l) => {
|
||||||
const hasTextPrompt = l.textPrompt && (l.textPrompt.positive || l.textPrompt.negative);
|
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
||||||
const hasAtLeastOneImagePrompt = l.imagePrompts.length > 0;
|
const hasIPAdapter = l.ipAdapterIds.length !== 0;
|
||||||
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
return hasTextPrompt || hasIPAdapter;
|
||||||
});
|
});
|
||||||
|
|
||||||
const layerIds = layers.map((l) => l.id);
|
const layerIds = layers.map((l) => l.id);
|
||||||
@ -103,6 +105,22 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!graph.nodes[IP_ADAPTER_COLLECT] && sumBy(layers, (l) => l.ipAdapterIds.length) > 0) {
|
||||||
|
const ipAdapterCollectNode: CollectInvocation = {
|
||||||
|
id: IP_ADAPTER_COLLECT,
|
||||||
|
type: 'collect',
|
||||||
|
is_intermediate: true,
|
||||||
|
};
|
||||||
|
graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode;
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' },
|
||||||
|
destination: {
|
||||||
|
node_id: denoiseNodeId,
|
||||||
|
field: 'ip_adapter',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Upload the blobs to the backend, add each to graph
|
// Upload the blobs to the backend, add each to graph
|
||||||
// TODO: Store the uploaded image names in redux to reuse them, so long as the layer hasn't otherwise changed. This
|
// TODO: Store the uploaded image names in redux to reuse them, so long as the layer hasn't otherwise changed. This
|
||||||
// would be a great perf win - not only would we skip re-uploading the same image, but we'd be able to use the node
|
// would be a great perf win - not only would we skip re-uploading the same image, but we'd be able to use the node
|
||||||
@ -130,19 +148,19 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
|||||||
};
|
};
|
||||||
graph.nodes[maskToTensorNode.id] = maskToTensorNode;
|
graph.nodes[maskToTensorNode.id] = maskToTensorNode;
|
||||||
|
|
||||||
if (layer.textPrompt?.positive) {
|
if (layer.positivePrompt) {
|
||||||
// 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.textPrompt.positive,
|
prompt: layer.positivePrompt,
|
||||||
style: layer.textPrompt.positive, // TODO: Should we put the positive prompt in both fields?
|
style: layer.positivePrompt, // 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.textPrompt.positive,
|
prompt: layer.positivePrompt,
|
||||||
};
|
};
|
||||||
graph.nodes[regionalPositiveCondNode.id] = regionalPositiveCondNode;
|
graph.nodes[regionalPositiveCondNode.id] = regionalPositiveCondNode;
|
||||||
|
|
||||||
@ -169,19 +187,19 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layer.textPrompt?.negative) {
|
if (layer.negativePrompt) {
|
||||||
// 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.textPrompt.negative,
|
prompt: layer.negativePrompt,
|
||||||
style: layer.textPrompt.negative,
|
style: layer.negativePrompt,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`,
|
id: `${PROMPT_REGION_NEGATIVE_COND_PREFIX}_${layer.id}`,
|
||||||
prompt: layer.textPrompt.negative,
|
prompt: layer.negativePrompt,
|
||||||
};
|
};
|
||||||
graph.nodes[regionalNegativeCondNode.id] = regionalNegativeCondNode;
|
graph.nodes[regionalNegativeCondNode.id] = regionalNegativeCondNode;
|
||||||
|
|
||||||
@ -209,7 +227,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.textPrompt?.positive) {
|
if (layer.autoNegative === 'invert' && layer.positivePrompt) {
|
||||||
// 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}`,
|
||||||
@ -235,13 +253,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.textPrompt.positive,
|
prompt: layer.positivePrompt,
|
||||||
style: layer.textPrompt.positive,
|
style: layer.positivePrompt,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'compel',
|
type: 'compel',
|
||||||
id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`,
|
id: `${PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX}_${layer.id}`,
|
||||||
prompt: layer.textPrompt.positive,
|
prompt: layer.positivePrompt,
|
||||||
};
|
};
|
||||||
graph.nodes[regionalPositiveCondInvertedNode.id] = regionalPositiveCondInvertedNode;
|
graph.nodes[regionalPositiveCondInvertedNode.id] = regionalPositiveCondInvertedNode;
|
||||||
// Connect the inverted mask to the conditioning
|
// Connect the inverted mask to the conditioning
|
||||||
@ -264,5 +282,47 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const ipAdapterId of layer.ipAdapterIds) {
|
||||||
|
const ipAdapter = selectAllIPAdapters(state.controlAdapters).find((ca) => ca.id === ipAdapterId);
|
||||||
|
console.log(ipAdapter);
|
||||||
|
if (!ipAdapter?.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { id, weight, model, clipVisionModel, method, beginStepPct, endStepPct, controlImage } = ipAdapter;
|
||||||
|
|
||||||
|
assert(controlImage, 'IP Adapter image is required');
|
||||||
|
|
||||||
|
const ipAdapterNode: IPAdapterInvocation = {
|
||||||
|
id: `ip_adapter_${id}`,
|
||||||
|
type: 'ip_adapter',
|
||||||
|
is_intermediate: true,
|
||||||
|
weight: weight,
|
||||||
|
method: method,
|
||||||
|
ip_adapter_model: model,
|
||||||
|
clip_vision_model: clipVisionModel,
|
||||||
|
begin_step_percent: beginStepPct,
|
||||||
|
end_step_percent: endStepPct,
|
||||||
|
image: {
|
||||||
|
image_name: controlImage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.nodes[ipAdapterNode.id] = ipAdapterNode;
|
||||||
|
|
||||||
|
// Connect the mask to the conditioning
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: maskToTensorNode.id, field: 'mask' },
|
||||||
|
destination: { node_id: ipAdapterNode.id, field: 'mask' },
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: ipAdapterNode.id, field: 'ip_adapter' },
|
||||||
|
destination: {
|
||||||
|
node_id: IP_ADAPTER_COLLECT,
|
||||||
|
field: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
import { Button, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
isVectorMaskLayer,
|
||||||
|
maskLayerIPAdapterAdded,
|
||||||
|
maskLayerNegativePromptChanged,
|
||||||
|
maskLayerPositivePromptChanged,
|
||||||
|
selectRegionalPromptsSlice,
|
||||||
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
type AddPromptButtonProps = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const selectValidActions = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||||
|
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
|
return {
|
||||||
|
canAddPositivePrompt: layer.positivePrompt === null,
|
||||||
|
canAddNegativePrompt: layer.negativePrompt === null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const validActions = useAppSelector(selectValidActions);
|
||||||
|
const addPositivePrompt = useCallback(() => {
|
||||||
|
dispatch(maskLayerPositivePromptChanged({ layerId, prompt: '' }));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const addNegativePrompt = useCallback(() => {
|
||||||
|
dispatch(maskLayerNegativePromptChanged({ layerId, prompt: '' }));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const addIPAdapter = useCallback(() => {
|
||||||
|
dispatch(maskLayerIPAdapterAdded(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w="full" p={2} justifyContent="space-between">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
leftIcon={<PiPlusBold />}
|
||||||
|
onClick={addPositivePrompt}
|
||||||
|
isDisabled={!validActions.canAddPositivePrompt}
|
||||||
|
>
|
||||||
|
{t('common.positivePrompt')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
leftIcon={<PiPlusBold />}
|
||||||
|
onClick={addNegativePrompt}
|
||||||
|
isDisabled={!validActions.canAddNegativePrompt}
|
||||||
|
>
|
||||||
|
{t('common.negativePrompt')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||||
|
{t('common.ipAdapter')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +1,22 @@
|
|||||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import {
|
||||||
|
CompositeNumberInput,
|
||||||
|
CompositeSlider,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { brushSizeChanged, initialRegionalPromptsState } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { brushSizeChanged, initialRegionalPromptsState } 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';
|
||||||
|
|
||||||
|
const marks = [0, 100, 200, 300];
|
||||||
|
const formatPx = (v: number | string) => `${v} px`;
|
||||||
|
|
||||||
export const BrushSize = memo(() => {
|
export const BrushSize = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -15,22 +28,34 @@ export const BrushSize = memo(() => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl w="min-content">
|
||||||
<FormLabel>{t('regionalPrompts.brushSize')}</FormLabel>
|
<FormLabel m={0}>{t('regionalPrompts.brushSize')}</FormLabel>
|
||||||
<CompositeSlider
|
<Popover isLazy>
|
||||||
min={1}
|
<PopoverTrigger>
|
||||||
max={300}
|
<CompositeNumberInput
|
||||||
defaultValue={initialRegionalPromptsState.brushSize}
|
min={1}
|
||||||
value={brushSize}
|
max={600}
|
||||||
onChange={onChange}
|
defaultValue={initialRegionalPromptsState.brushSize}
|
||||||
/>
|
value={brushSize}
|
||||||
<CompositeNumberInput
|
onChange={onChange}
|
||||||
min={1}
|
w={24}
|
||||||
max={600}
|
format={formatPx}
|
||||||
defaultValue={initialRegionalPromptsState.brushSize}
|
/>
|
||||||
value={brushSize}
|
</PopoverTrigger>
|
||||||
onChange={onChange}
|
<PopoverContent w={200} py={2} px={4}>
|
||||||
/>
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<CompositeSlider
|
||||||
|
min={1}
|
||||||
|
max={300}
|
||||||
|
defaultValue={initialRegionalPromptsState.brushSize}
|
||||||
|
value={brushSize}
|
||||||
|
onChange={onChange}
|
||||||
|
marks={marks}
|
||||||
|
/>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import {
|
||||||
|
CompositeNumberInput,
|
||||||
|
CompositeSlider,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
globalMaskLayerOpacityChanged,
|
globalMaskLayerOpacityChanged,
|
||||||
@ -7,35 +17,52 @@ import {
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const marks = [0, 25, 50, 75, 100];
|
||||||
|
const formatPct = (v: number | string) => `${v} %`;
|
||||||
|
|
||||||
export const GlobalMaskLayerOpacity = memo(() => {
|
export const GlobalMaskLayerOpacity = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const globalMaskLayerOpacity = useAppSelector((s) => s.regionalPrompts.present.globalMaskLayerOpacity);
|
const globalMaskLayerOpacity = useAppSelector((s) =>
|
||||||
|
Math.round(s.regionalPrompts.present.globalMaskLayerOpacity * 100)
|
||||||
|
);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(globalMaskLayerOpacityChanged(v));
|
dispatch(globalMaskLayerOpacityChanged(v / 100));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl w="min-content">
|
||||||
<FormLabel>{t('regionalPrompts.layerOpacity')}</FormLabel>
|
<FormLabel m={0}>{t('regionalPrompts.globalMaskOpacity')}</FormLabel>
|
||||||
<CompositeSlider
|
<Popover isLazy>
|
||||||
min={0.25}
|
<PopoverTrigger>
|
||||||
max={1}
|
<CompositeNumberInput
|
||||||
step={0.01}
|
min={0}
|
||||||
value={globalMaskLayerOpacity}
|
max={100}
|
||||||
defaultValue={initialRegionalPromptsState.globalMaskLayerOpacity}
|
step={1}
|
||||||
onChange={onChange}
|
value={globalMaskLayerOpacity}
|
||||||
/>
|
defaultValue={initialRegionalPromptsState.globalMaskLayerOpacity * 100}
|
||||||
<CompositeNumberInput
|
onChange={onChange}
|
||||||
min={0.25}
|
w={24}
|
||||||
max={1}
|
format={formatPct}
|
||||||
step={0.01}
|
/>
|
||||||
value={globalMaskLayerOpacity}
|
</PopoverTrigger>
|
||||||
defaultValue={initialRegionalPromptsState.globalMaskLayerOpacity}
|
<PopoverContent w={200} py={2} px={4}>
|
||||||
onChange={onChange}
|
<PopoverArrow />
|
||||||
/>
|
<PopoverBody>
|
||||||
|
<CompositeSlider
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={globalMaskLayerOpacity}
|
||||||
|
defaultValue={initialRegionalPromptsState.globalMaskLayerOpacity * 100}
|
||||||
|
onChange={onChange}
|
||||||
|
marks={marks}
|
||||||
|
/>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { layerDeleted, layerReset } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiArrowCounterClockwiseBold, PiTrashSimpleBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
type Props = { layerId: string };
|
|
||||||
|
|
||||||
export const RPLayerActionsButtonGroup = memo(({ layerId }: Props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const deleteLayer = useCallback(() => {
|
|
||||||
dispatch(layerDeleted(layerId));
|
|
||||||
}, [dispatch, layerId]);
|
|
||||||
const resetLayer = useCallback(() => {
|
|
||||||
dispatch(layerReset(layerId));
|
|
||||||
}, [dispatch, layerId]);
|
|
||||||
return (
|
|
||||||
<ButtonGroup isAttached={false}>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
aria-label={t('regionalPrompts.resetRegion')}
|
|
||||||
tooltip={t('regionalPrompts.resetRegion')}
|
|
||||||
icon={<PiArrowCounterClockwiseBold />}
|
|
||||||
onClick={resetLayer}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
colorScheme="error"
|
|
||||||
aria-label={t('common.delete')}
|
|
||||||
tooltip={t('common.delete')}
|
|
||||||
icon={<PiTrashSimpleBold />}
|
|
||||||
onClick={deleteLayer}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
RPLayerActionsButtonGroup.displayName = 'RPLayerActionsButtonGroup';
|
|
@ -1,22 +1,16 @@
|
|||||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
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 {
|
import {
|
||||||
isVectorMaskLayer,
|
isVectorMaskLayer,
|
||||||
maskLayerAutoNegativeChanged,
|
maskLayerAutoNegativeChanged,
|
||||||
selectRegionalPromptsSlice,
|
selectRegionalPromptsSlice,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
const options: ComboboxOption[] = [
|
|
||||||
{ label: 'Off', value: 'off' },
|
|
||||||
{ label: 'Invert', value: 'invert' },
|
|
||||||
];
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
@ -35,29 +29,23 @@ const useAutoNegative = (layerId: string) => {
|
|||||||
return autoNegative;
|
return autoNegative;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerAutoNegativeCombobox = memo(({ layerId }: Props) => {
|
export const RPLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const autoNegative = useAutoNegative(layerId);
|
const autoNegative = useAutoNegative(layerId);
|
||||||
|
const onChange = useCallback(
|
||||||
const onChange = useCallback<ComboboxOnChange>(
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
(v) => {
|
dispatch(maskLayerAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' }));
|
||||||
if (!isParameterAutoNegative(v?.value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(maskLayerAutoNegativeChanged({ layerId, autoNegative: v.value }));
|
|
||||||
},
|
},
|
||||||
[dispatch, layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(() => options.find((o) => o.value === autoNegative), [autoNegative]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl flexGrow={0} gap={2} w="min-content">
|
<FormControl gap={2}>
|
||||||
<FormLabel m={0}>{t('regionalPrompts.autoNegative')}</FormLabel>
|
<FormLabel m={0}>{t('regionalPrompts.autoNegative')}</FormLabel>
|
||||||
<Combobox value={value} options={options} onChange={onChange} isSearchable={false} sx={{ w: '5.2rem' }} />
|
<Checkbox size="md" isChecked={autoNegative === 'invert'} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerAutoNegativeCombobox.displayName = 'RPLayerAutoNegativeCombobox';
|
RPLayerAutoNegativeCheckbox.displayName = 'RPLayerAutoNegativeCheckbox';
|
@ -1,16 +1,16 @@
|
|||||||
import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } 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 IAIColorPicker from 'common/components/IAIColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import {
|
import {
|
||||||
isVectorMaskLayer,
|
isVectorMaskLayer,
|
||||||
maskLayerPreviewColorChanged,
|
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 { RgbaColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEyedropperBold } from 'react-icons/pi';
|
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -31,7 +31,7 @@ export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
|||||||
const color = useAppSelector(selectColor);
|
const color = useAppSelector(selectColor);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onColorChange = useCallback(
|
const onColorChange = useCallback(
|
||||||
(color: RgbaColor) => {
|
(color: RgbColor) => {
|
||||||
dispatch(maskLayerPreviewColorChanged({ layerId, color }));
|
dispatch(maskLayerPreviewColorChanged({ layerId, color }));
|
||||||
},
|
},
|
||||||
[dispatch, layerId]
|
[dispatch, layerId]
|
||||||
@ -39,17 +39,25 @@ export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton
|
<span>
|
||||||
tooltip={t('unifiedCanvas.colorPicker')}
|
<Tooltip label={t('regionalPrompts.maskPreviewColor')}>
|
||||||
aria-label={t('unifiedCanvas.colorPicker')}
|
<Flex
|
||||||
size="sm"
|
as="button"
|
||||||
borderRadius="base"
|
aria-label={t('regionalPrompts.maskPreviewColor')}
|
||||||
icon={<PiEyedropperBold />}
|
borderRadius="base"
|
||||||
/>
|
borderWidth={1}
|
||||||
|
bg={rgbColorToString(color)}
|
||||||
|
w={8}
|
||||||
|
h={8}
|
||||||
|
cursor="pointer"
|
||||||
|
tabIndex={-1}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<PopoverBody minH={64}>
|
<PopoverBody minH={64}>
|
||||||
<IAIColorPicker color={color} onChange={onColorChange} withNumberInput />
|
<RgbColorPicker color={color} onChange={onColorChange} withNumberInput />
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { layerDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
type Props = { layerId: string };
|
||||||
|
|
||||||
|
export const RPLayerDeleteButton = memo(({ layerId }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const deleteLayer = useCallback(() => {
|
||||||
|
dispatch(layerDeleted(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
colorScheme="error"
|
||||||
|
aria-label={t('common.delete')}
|
||||||
|
tooltip={t('common.delete')}
|
||||||
|
icon={<PiTrashSimpleBold />}
|
||||||
|
onClick={deleteLayer}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RPLayerDeleteButton.displayName = 'RPLayerDeleteButton';
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import ControlAdapterConfig from 'features/controlAdapters/components/ControlAdapterConfig';
|
||||||
|
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RPLayerIPAdapterList = memo(({ layerId }: Props) => {
|
||||||
|
const selectIPAdapterIds = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||||
|
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(layer, `Layer ${layerId} not found`);
|
||||||
|
return layer.ipAdapterIds;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const ipAdapterIds = useAppSelector(selectIPAdapterIds);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w="full" flexDir="column" gap={2}>
|
||||||
|
{ipAdapterIds.map((id, index) => (
|
||||||
|
<ControlAdapterConfig key={id} id={id} number={index + 1} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RPLayerIPAdapterList.displayName = 'RPLayerIPAdapterList';
|
@ -1,34 +1,51 @@
|
|||||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Badge, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import { RPLayerActionsButtonGroup } from 'features/regionalPrompts/components/RPLayerActionsButtonGroup';
|
|
||||||
import { RPLayerAutoNegativeCombobox } from 'features/regionalPrompts/components/RPLayerAutoNegativeCombobox';
|
|
||||||
import { RPLayerColorPicker } from 'features/regionalPrompts/components/RPLayerColorPicker';
|
import { RPLayerColorPicker } from 'features/regionalPrompts/components/RPLayerColorPicker';
|
||||||
|
import { RPLayerDeleteButton } from 'features/regionalPrompts/components/RPLayerDeleteButton';
|
||||||
|
import { RPLayerIPAdapterList } from 'features/regionalPrompts/components/RPLayerIPAdapterList';
|
||||||
import { RPLayerMenu } from 'features/regionalPrompts/components/RPLayerMenu';
|
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 RPLayerSettingsPopover from 'features/regionalPrompts/components/RPLayerSettingsPopover';
|
||||||
import { RPLayerVisibilityToggle } from 'features/regionalPrompts/components/RPLayerVisibilityToggle';
|
import { RPLayerVisibilityToggle } from 'features/regionalPrompts/components/RPLayerVisibilityToggle';
|
||||||
import { isVectorMaskLayer, layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import {
|
||||||
import { memo, useCallback } from 'react';
|
isVectorMaskLayer,
|
||||||
|
layerSelected,
|
||||||
|
selectRegionalPromptsSlice,
|
||||||
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
import { AddPromptButtons } from './AddPromptButtons';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerListItem = memo(({ layerId }: Props) => {
|
export const RPLayerListItem = memo(({ layerId }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId);
|
const selector = useMemo(
|
||||||
const color = useAppSelector((s) => {
|
() =>
|
||||||
const layer = s.regionalPrompts.present.layers.find((l) => l.id === layerId);
|
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||||
return rgbColorToString(layer.previewColor);
|
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
});
|
return {
|
||||||
const hasTextPrompt = useAppSelector((s) => {
|
color: rgbColorToString(layer.previewColor),
|
||||||
const layer = s.regionalPrompts.present.layers.find((l) => l.id === layerId);
|
hasPositivePrompt: layer.positivePrompt !== null,
|
||||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
hasNegativePrompt: layer.negativePrompt !== null,
|
||||||
return layer.textPrompt !== null;
|
hasIPAdapters: layer.ipAdapterIds.length > 0,
|
||||||
});
|
isSelected: layerId === regionalPrompts.present.selectedLayerId,
|
||||||
|
autoNegative: layer.autoNegative,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } =
|
||||||
|
useAppSelector(selector);
|
||||||
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(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
@ -37,26 +54,31 @@ export const RPLayerListItem = memo(({ layerId }: Props) => {
|
|||||||
<Flex
|
<Flex
|
||||||
gap={2}
|
gap={2}
|
||||||
onClickCapture={onClickCapture}
|
onClickCapture={onClickCapture}
|
||||||
bg={color}
|
bg={isSelected ? color : 'base.800'}
|
||||||
px={2}
|
ps={2}
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
borderTop="1px"
|
pe="1px"
|
||||||
borderBottom="1px"
|
py="1px"
|
||||||
borderColor="base.800"
|
|
||||||
opacity={selectedLayerId === layerId ? 1 : 0.5}
|
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
>
|
>
|
||||||
<Flex flexDir="column" gap={2} w="full" bg="base.850" p={2}>
|
<Flex flexDir="column" gap={2} w="full" bg="base.850" p={2} borderRadius="base">
|
||||||
<Flex gap={2} alignItems="center">
|
<Flex gap={3} alignItems="center">
|
||||||
<RPLayerMenu layerId={layerId} />
|
|
||||||
<RPLayerColorPicker layerId={layerId} />
|
|
||||||
<RPLayerVisibilityToggle layerId={layerId} />
|
<RPLayerVisibilityToggle layerId={layerId} />
|
||||||
|
<RPLayerColorPicker layerId={layerId} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<RPLayerAutoNegativeCombobox layerId={layerId} />
|
{autoNegative === 'invert' && (
|
||||||
<RPLayerActionsButtonGroup layerId={layerId} />
|
<Badge color="base.300" bg="transparent" borderWidth={1}>
|
||||||
|
{t('regionalPrompts.autoNegative')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<RPLayerDeleteButton layerId={layerId} />
|
||||||
|
<RPLayerSettingsPopover layerId={layerId} />
|
||||||
|
<RPLayerMenu layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
{hasTextPrompt && <RPLayerPositivePrompt layerId={layerId} />}
|
<AddPromptButtons layerId={layerId} />
|
||||||
{hasTextPrompt && <RPLayerNegativePrompt layerId={layerId} />}
|
{hasPositivePrompt && <RPLayerPositivePrompt layerId={layerId} />}
|
||||||
|
{hasNegativePrompt && <RPLayerNegativePrompt layerId={layerId} />}
|
||||||
|
{hasIPAdapters && <RPLayerIPAdapterList layerId={layerId} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,9 @@ import {
|
|||||||
layerMovedToBack,
|
layerMovedToBack,
|
||||||
layerMovedToFront,
|
layerMovedToFront,
|
||||||
layerReset,
|
layerReset,
|
||||||
|
maskLayerIPAdapterAdded,
|
||||||
|
maskLayerNegativePromptChanged,
|
||||||
|
maskLayerPositivePromptChanged,
|
||||||
selectRegionalPromptsSlice,
|
selectRegionalPromptsSlice,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
@ -20,6 +23,7 @@ import {
|
|||||||
PiArrowLineUpBold,
|
PiArrowLineUpBold,
|
||||||
PiArrowUpBold,
|
PiArrowUpBold,
|
||||||
PiDotsThreeVerticalBold,
|
PiDotsThreeVerticalBold,
|
||||||
|
PiPlusBold,
|
||||||
PiTrashSimpleBold,
|
PiTrashSimpleBold,
|
||||||
} from 'react-icons/pi';
|
} from 'react-icons/pi';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
@ -37,6 +41,8 @@ export const RPLayerMenu = memo(({ layerId }: Props) => {
|
|||||||
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 {
|
||||||
|
canAddPositivePrompt: layer.positivePrompt === null,
|
||||||
|
canAddNegativePrompt: layer.negativePrompt === null,
|
||||||
canMoveForward: layerIndex < layerCount - 1,
|
canMoveForward: layerIndex < layerCount - 1,
|
||||||
canMoveBackward: layerIndex > 0,
|
canMoveBackward: layerIndex > 0,
|
||||||
canMoveToFront: layerIndex < layerCount - 1,
|
canMoveToFront: layerIndex < layerCount - 1,
|
||||||
@ -46,6 +52,15 @@ export const RPLayerMenu = memo(({ layerId }: Props) => {
|
|||||||
[layerId]
|
[layerId]
|
||||||
);
|
);
|
||||||
const validActions = useAppSelector(selectValidActions);
|
const validActions = useAppSelector(selectValidActions);
|
||||||
|
const addPositivePrompt = useCallback(() => {
|
||||||
|
dispatch(maskLayerPositivePromptChanged({ layerId, prompt: '' }));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const addNegativePrompt = useCallback(() => {
|
||||||
|
dispatch(maskLayerNegativePromptChanged({ layerId, prompt: '' }));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const addIPAdapter = useCallback(() => {
|
||||||
|
dispatch(maskLayerIPAdapterAdded(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
const moveForward = useCallback(() => {
|
const moveForward = useCallback(() => {
|
||||||
dispatch(layerMovedForward(layerId));
|
dispatch(layerMovedForward(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
@ -68,6 +83,16 @@ export const RPLayerMenu = memo(({ layerId }: Props) => {
|
|||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton as={IconButton} aria-label="Layer menu" size="sm" icon={<PiDotsThreeVerticalBold />} />
|
<MenuButton as={IconButton} aria-label="Layer menu" size="sm" icon={<PiDotsThreeVerticalBold />} />
|
||||||
<MenuList>
|
<MenuList>
|
||||||
|
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt} icon={<PiPlusBold />}>
|
||||||
|
{t('regionalPrompts.addPositivePrompt')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt} icon={<PiPlusBold />}>
|
||||||
|
{t('regionalPrompts.addNegativePrompt')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={addIPAdapter} icon={<PiPlusBold />}>
|
||||||
|
{t('regionalPrompts.addIPAdapter')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
|
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
|
||||||
{t('regionalPrompts.moveToFront')}
|
{t('regionalPrompts.moveToFront')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -4,42 +4,32 @@ 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 { useMaskLayerTextPrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
import { RPLayerPromptDeleteButton } from 'features/regionalPrompts/components/RPLayerPromptDeleteButton';
|
||||||
|
import { useLayerNegativePrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
||||||
import { maskLayerNegativePromptChanged } 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 { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerNegativePrompt = memo((props: Props) => {
|
export const RPLayerNegativePrompt = memo(({ layerId }: Props) => {
|
||||||
const textPrompt = useMaskLayerTextPrompt(props.layerId);
|
const prompt = useLayerNegativePrompt(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(maskLayerNegativePromptChanged({ layerId: props.layerId, prompt: v }));
|
dispatch(maskLayerNegativePromptChanged({ layerId, prompt: v }));
|
||||||
},
|
},
|
||||||
[dispatch, props.layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
|
||||||
prompt: textPrompt.negative,
|
prompt,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
onChange: _onChange,
|
onChange: _onChange,
|
||||||
});
|
});
|
||||||
const focus: HotkeyCallback = useCallback(
|
|
||||||
(e) => {
|
|
||||||
onFocus();
|
|
||||||
e.preventDefault();
|
|
||||||
},
|
|
||||||
[onFocus]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys('alt+a', focus, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||||
@ -48,7 +38,7 @@ export const RPLayerNegativePrompt = memo((props: Props) => {
|
|||||||
id="prompt"
|
id="prompt"
|
||||||
name="prompt"
|
name="prompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={textPrompt.negative}
|
value={prompt}
|
||||||
placeholder={t('parameters.negativePromptPlaceholder')}
|
placeholder={t('parameters.negativePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
@ -57,6 +47,7 @@ export const RPLayerNegativePrompt = memo((props: Props) => {
|
|||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
|
<RPLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -4,42 +4,32 @@ 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 { useMaskLayerTextPrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
import { RPLayerPromptDeleteButton } from 'features/regionalPrompts/components/RPLayerPromptDeleteButton';
|
||||||
|
import { useLayerPositivePrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
||||||
import { maskLayerPositivePromptChanged } 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 { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerPositivePrompt = memo((props: Props) => {
|
export const RPLayerPositivePrompt = memo(({ layerId }: Props) => {
|
||||||
const textPrompt = useMaskLayerTextPrompt(props.layerId);
|
const prompt = useLayerPositivePrompt(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(maskLayerPositivePromptChanged({ layerId: props.layerId, prompt: v }));
|
dispatch(maskLayerPositivePromptChanged({ layerId, prompt: v }));
|
||||||
},
|
},
|
||||||
[dispatch, props.layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({
|
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
|
||||||
prompt: textPrompt.positive,
|
prompt,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
onChange: _onChange,
|
onChange: _onChange,
|
||||||
});
|
});
|
||||||
const focus: HotkeyCallback = useCallback(
|
|
||||||
(e) => {
|
|
||||||
onFocus();
|
|
||||||
e.preventDefault();
|
|
||||||
},
|
|
||||||
[onFocus]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys('alt+a', focus, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
<PromptPopover isOpen={isOpen} onClose={onClose} onSelect={onSelect} width={textareaRef.current?.clientWidth}>
|
||||||
@ -48,7 +38,7 @@ export const RPLayerPositivePrompt = memo((props: Props) => {
|
|||||||
id="prompt"
|
id="prompt"
|
||||||
name="prompt"
|
name="prompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={textPrompt.positive}
|
value={prompt}
|
||||||
placeholder={t('parameters.positivePromptPlaceholder')}
|
placeholder={t('parameters.positivePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
@ -57,6 +47,7 @@ export const RPLayerPositivePrompt = memo((props: Props) => {
|
|||||||
minH={28}
|
minH={28}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
|
<RPLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
maskLayerNegativePromptChanged,
|
||||||
|
maskLayerPositivePromptChanged,
|
||||||
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
polarity: 'positive' | 'negative';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RPLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (polarity === 'positive') {
|
||||||
|
dispatch(maskLayerPositivePromptChanged({ layerId, prompt: null }));
|
||||||
|
} else {
|
||||||
|
dispatch(maskLayerNegativePromptChanged({ layerId, prompt: null }));
|
||||||
|
}
|
||||||
|
}, [dispatch, layerId, polarity]);
|
||||||
|
return (
|
||||||
|
<Tooltip label={t('regionalPrompts.deletePrompt')}>
|
||||||
|
<IconButton
|
||||||
|
variant="promptOverlay"
|
||||||
|
aria-label={t('regionalPrompts.deletePrompt')}
|
||||||
|
icon={<PiTrashSimpleBold />}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RPLayerPromptDeleteButton.displayName = 'RPLayerPromptDeleteButton';
|
@ -0,0 +1,53 @@
|
|||||||
|
import type { FormLabelProps } from '@invoke-ai/ui-library';
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
FormControlGroup,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { RPLayerAutoNegativeCheckbox } from 'features/regionalPrompts/components/RPLayerAutoNegativeCheckbox';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiGearSixBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formLabelProps: FormLabelProps = {
|
||||||
|
flexGrow: 1,
|
||||||
|
minW: 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
const RPLayerSettingsPopover = ({ layerId }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover isLazy>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton
|
||||||
|
tooltip={t('common.settingsLabel')}
|
||||||
|
aria-label={t('common.settingsLabel')}
|
||||||
|
size="sm"
|
||||||
|
icon={<PiGearSixBold />}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex direction="column" gap={2}>
|
||||||
|
<FormControlGroup formLabelProps={formLabelProps}>
|
||||||
|
<RPLayerAutoNegativeCheckbox layerId={layerId} />
|
||||||
|
</FormControlGroup>
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(RPLayerSettingsPopover);
|
@ -4,7 +4,7 @@ import { useLayerIsVisible } from 'features/regionalPrompts/hooks/layerStateHook
|
|||||||
import { layerVisibilityToggled } 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 { PiCheckBold } from 'react-icons/pi';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
@ -23,9 +23,10 @@ export const RPLayerVisibilityToggle = memo(({ layerId }: Props) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
aria-label={t('regionalPrompts.toggleVisibility')}
|
aria-label={t('regionalPrompts.toggleVisibility')}
|
||||||
tooltip={t('regionalPrompts.toggleVisibility')}
|
tooltip={t('regionalPrompts.toggleVisibility')}
|
||||||
variant={isVisible ? 'outline' : 'ghost'}
|
variant="outline"
|
||||||
icon={isVisible ? <PiEyeBold /> : <PiEyeClosedBold />}
|
icon={isVisible ? <PiCheckBold /> : undefined}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
colorScheme="base"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -5,10 +5,8 @@ import { useAppSelector } from 'app/store/storeHooks';
|
|||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
|
import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton';
|
||||||
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
|
import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
|
||||||
import { DebugLayersButton } from 'features/regionalPrompts/components/DebugLayersButton';
|
|
||||||
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
|
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
|
||||||
import { GlobalMaskLayerOpacity } from 'features/regionalPrompts/components/GlobalMaskLayerOpacity';
|
import { GlobalMaskLayerOpacity } from 'features/regionalPrompts/components/GlobalMaskLayerOpacity';
|
||||||
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';
|
||||||
@ -29,16 +27,16 @@ export const RegionalPromptsEditor = memo(() => {
|
|||||||
<Flex gap={4} w="full" h="full">
|
<Flex gap={4} w="full" h="full">
|
||||||
<Flex flexDir="column" gap={4} minW={430}>
|
<Flex flexDir="column" gap={4} minW={430}>
|
||||||
<Flex gap={3} w="full" justifyContent="space-between">
|
<Flex gap={3} w="full" justifyContent="space-between">
|
||||||
<DebugLayersButton />
|
|
||||||
<AddLayerButton />
|
<AddLayerButton />
|
||||||
<DeleteAllLayersButton />
|
<DeleteAllLayersButton />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<UndoRedoButtonGroup />
|
<UndoRedoButtonGroup />
|
||||||
<ToolChooser />
|
<ToolChooser />
|
||||||
</Flex>
|
</Flex>
|
||||||
<RPEnabledSwitch />
|
<Flex justifyContent="space-between">
|
||||||
<BrushSize />
|
<BrushSize />
|
||||||
<GlobalMaskLayerOpacity />
|
<GlobalMaskLayerOpacity />
|
||||||
|
</Flex>
|
||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2}>
|
||||||
{rpLayerIdsReversed.map((id) => (
|
{rpLayerIdsReversed.map((id) => (
|
||||||
|
@ -21,6 +21,9 @@ import { atom } from 'nanostores';
|
|||||||
import { useCallback, useLayoutEffect } from 'react';
|
import { useCallback, useLayoutEffect } from 'react';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||||
|
Konva.showWarnings = false;
|
||||||
|
|
||||||
const log = logger('regionalPrompts');
|
const log = logger('regionalPrompts');
|
||||||
const $stage = atom<Konva.Stage | null>(null);
|
const $stage = atom<Konva.Stage | null>(null);
|
||||||
const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||||
@ -132,16 +135,32 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
|||||||
if (!stage) {
|
if (!stage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderToolPreview(stage, tool, selectedLayerIdColor, cursorPosition, lastMouseDownPos, state.brushSize);
|
renderToolPreview(
|
||||||
}, [stage, tool, selectedLayerIdColor, cursorPosition, lastMouseDownPos, state.brushSize]);
|
stage,
|
||||||
|
tool,
|
||||||
|
selectedLayerIdColor,
|
||||||
|
state.globalMaskLayerOpacity,
|
||||||
|
cursorPosition,
|
||||||
|
lastMouseDownPos,
|
||||||
|
state.brushSize
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
stage,
|
||||||
|
tool,
|
||||||
|
selectedLayerIdColor,
|
||||||
|
state.globalMaskLayerOpacity,
|
||||||
|
cursorPosition,
|
||||||
|
lastMouseDownPos,
|
||||||
|
state.brushSize,
|
||||||
|
]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering layers');
|
log.trace('Rendering layers');
|
||||||
if (!stage) {
|
if (!stage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderLayers(stage, state.layers, tool, onLayerPosChanged);
|
renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged);
|
||||||
}, [stage, state.layers, tool, onLayerPosChanged]);
|
}, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering bbox');
|
log.trace('Rendering bbox');
|
||||||
|
@ -4,19 +4,34 @@ import { isVectorMaskLayer, selectRegionalPromptsSlice } from 'features/regional
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const useMaskLayerTextPrompt = (layerId: string) => {
|
export const useLayerPositivePrompt = (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(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
assert(layer.textPrompt !== null, `Layer ${layerId} does not have a text prompt`);
|
assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`);
|
||||||
return layer.textPrompt;
|
return layer.positivePrompt;
|
||||||
}),
|
}),
|
||||||
[layerId]
|
[layerId]
|
||||||
);
|
);
|
||||||
const textPrompt = useAppSelector(selectLayer);
|
const prompt = useAppSelector(selectLayer);
|
||||||
return textPrompt;
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayerNegativePrompt = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
||||||
|
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
|
assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`);
|
||||||
|
return layer.negativePrompt;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const prompt = useAppSelector(selectLayer);
|
||||||
|
return prompt;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLayerIsVisible = (layerId: string) => {
|
export const useLayerIsVisible = (layerId: string) => {
|
||||||
|
@ -2,11 +2,12 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
|||||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
|
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
|
||||||
|
import { controlAdapterRemoved } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
||||||
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 { RgbaColor } from 'react-colorful';
|
import type { RgbColor } 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';
|
||||||
@ -32,15 +33,6 @@ type VectorMaskRect = {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TextPrompt = {
|
|
||||||
positive: string;
|
|
||||||
negative: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ImagePrompt = {
|
|
||||||
// TODO
|
|
||||||
};
|
|
||||||
|
|
||||||
type LayerBase = {
|
type LayerBase = {
|
||||||
id: string;
|
id: string;
|
||||||
x: number;
|
x: number;
|
||||||
@ -51,9 +43,10 @@ type LayerBase = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MaskLayerBase = LayerBase & {
|
type MaskLayerBase = LayerBase & {
|
||||||
textPrompt: TextPrompt | null; // Up to one text prompt per mask
|
positivePrompt: string | null;
|
||||||
imagePrompts: ImagePrompt[]; // Any number of image prompts
|
negativePrompt: string | null; // Up to one text prompt per mask
|
||||||
previewColor: RgbaColor;
|
ipAdapterIds: string[]; // Any number of image prompts
|
||||||
|
previewColor: RgbColor;
|
||||||
autoNegative: ParameterAutoNegative;
|
autoNegative: ParameterAutoNegative;
|
||||||
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object
|
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object
|
||||||
};
|
};
|
||||||
@ -70,7 +63,6 @@ type RegionalPromptsState = {
|
|||||||
selectedLayerId: string | null;
|
selectedLayerId: string | null;
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
brushColor: RgbaColor;
|
|
||||||
globalMaskLayerOpacity: number;
|
globalMaskLayerOpacity: number;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
};
|
};
|
||||||
@ -79,10 +71,9 @@ 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: [],
|
||||||
globalMaskLayerOpacity: 0.5, // this globally changes all mask layers' opacity
|
globalMaskLayerOpacity: 0.5, // this globally changes all mask layers' opacity
|
||||||
isEnabled: false,
|
isEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line';
|
const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line';
|
||||||
@ -98,7 +89,7 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
const kind = action.payload;
|
const kind = action.payload;
|
||||||
if (action.payload === 'vector_mask_layer') {
|
if (action.payload === 'vector_mask_layer') {
|
||||||
const lastColor = state.layers[state.layers.length - 1]?.previewColor;
|
const lastColor = state.layers[state.layers.length - 1]?.previewColor;
|
||||||
const color = LayerColors.next(lastColor);
|
const previewColor = LayerColors.next(lastColor);
|
||||||
const layer: VectorMaskLayer = {
|
const layer: VectorMaskLayer = {
|
||||||
id: getVectorMaskLayerId(action.meta.uuid),
|
id: getVectorMaskLayerId(action.meta.uuid),
|
||||||
type: kind,
|
type: kind,
|
||||||
@ -106,16 +97,14 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
bbox: null,
|
bbox: null,
|
||||||
bboxNeedsUpdate: false,
|
bboxNeedsUpdate: false,
|
||||||
objects: [],
|
objects: [],
|
||||||
previewColor: color,
|
previewColor,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
autoNegative: 'off',
|
autoNegative: 'invert',
|
||||||
needsPixelBbox: false,
|
needsPixelBbox: false,
|
||||||
textPrompt: {
|
positivePrompt: null,
|
||||||
positive: '',
|
negativePrompt: null,
|
||||||
negative: '',
|
ipAdapterIds: [],
|
||||||
},
|
|
||||||
imagePrompts: [],
|
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
state.selectedLayerId = layer.id;
|
state.selectedLayerId = layer.id;
|
||||||
@ -191,21 +180,30 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Mask Layers
|
//#region Mask Layers
|
||||||
maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
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 (layer && layer.textPrompt) {
|
if (layer) {
|
||||||
layer.textPrompt.positive = prompt;
|
layer.positivePrompt = prompt;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maskLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => {
|
maskLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
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 (layer && layer.textPrompt) {
|
if (layer) {
|
||||||
layer.textPrompt.negative = prompt;
|
layer.negativePrompt = prompt;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maskLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbaColor }>) => {
|
maskLayerIPAdapterAdded: {
|
||||||
|
reducer: (state, action: PayloadAction<string, string, { uuid: string }>) => {
|
||||||
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
|
if (layer) {
|
||||||
|
layer.ipAdapterIds.push(action.meta.uuid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: string) => ({ payload, meta: { uuid: uuidv4() } }),
|
||||||
|
},
|
||||||
|
maskLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
|
||||||
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 (layer) {
|
if (layer) {
|
||||||
@ -300,9 +298,6 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
globalMaskLayerOpacityChanged: (state, action: PayloadAction<number>) => {
|
globalMaskLayerOpacityChanged: (state, action: PayloadAction<number>) => {
|
||||||
state.globalMaskLayerOpacity = 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;
|
||||||
@ -321,28 +316,35 @@ export const regionalPromptsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
},
|
||||||
|
extraReducers(builder) {
|
||||||
|
builder.addCase(controlAdapterRemoved, (state, action) => {
|
||||||
|
for (const layer of state.layers) {
|
||||||
|
layer.ipAdapterIds = layer.ipAdapterIds.filter((id) => id !== action.payload.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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: RgbaColor[] = [
|
static COLORS: RgbColor[] = [
|
||||||
{ r: 123, g: 159, b: 237, a: 1 }, // rgb(123, 159, 237)
|
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
|
||||||
{ r: 106, g: 222, b: 106, a: 1 }, // rgb(106, 222, 106)
|
{ r: 131, g: 214, b: 131 }, // rgb(131, 214, 131)
|
||||||
{ r: 250, g: 225, b: 80, a: 1 }, // rgb(250, 225, 80)
|
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
|
||||||
{ r: 233, g: 137, b: 81, a: 1 }, // rgb(233, 137, 81)
|
{ r: 220, g: 144, b: 101 }, // rgb(220, 144, 101)
|
||||||
{ r: 229, g: 96, b: 96, a: 1 }, // rgb(229, 96, 96)
|
{ r: 224, g: 117, b: 117 }, // rgb(224, 117, 117)
|
||||||
{ r: 226, g: 122, b: 210, a: 1 }, // rgb(226, 122, 210)
|
{ r: 213, g: 139, b: 202 }, // rgb(213, 139, 202)
|
||||||
{ r: 167, g: 116, b: 234, a: 1 }, // rgb(167, 116, 234)
|
{ r: 161, g: 120, b: 214 }, // rgb(161, 120, 214)
|
||||||
];
|
];
|
||||||
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?: RgbaColor): RgbaColor {
|
static next(currentColor?: RgbColor): RgbColor {
|
||||||
if (currentColor) {
|
if (currentColor) {
|
||||||
const i = this.COLORS.findIndex((c) => isEqual(c, { ...currentColor, a: 1 }));
|
const i = this.COLORS.findIndex((c) => isEqual(c, currentColor));
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
this.i = i;
|
this.i = i;
|
||||||
}
|
}
|
||||||
@ -369,13 +371,14 @@ export const {
|
|||||||
layerVisibilityToggled,
|
layerVisibilityToggled,
|
||||||
allLayersDeleted,
|
allLayersDeleted,
|
||||||
// Mask layer actions
|
// Mask layer actions
|
||||||
|
maskLayerLineAdded,
|
||||||
|
maskLayerPointsAdded,
|
||||||
|
maskLayerRectAdded,
|
||||||
|
maskLayerNegativePromptChanged,
|
||||||
|
maskLayerPositivePromptChanged,
|
||||||
|
maskLayerIPAdapterAdded,
|
||||||
maskLayerAutoNegativeChanged,
|
maskLayerAutoNegativeChanged,
|
||||||
maskLayerPreviewColorChanged,
|
maskLayerPreviewColorChanged,
|
||||||
maskLayerLineAdded,
|
|
||||||
maskLayerNegativePromptChanged,
|
|
||||||
maskLayerPointsAdded,
|
|
||||||
maskLayerPositivePromptChanged,
|
|
||||||
maskLayerRectAdded,
|
|
||||||
// General actions
|
// General actions
|
||||||
isEnabledChanged,
|
isEnabledChanged,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
|
@ -6,6 +6,8 @@ import type { Layer as KonvaLayerType } from 'konva/lib/Layer';
|
|||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
export const GET_CLIENT_RECT_CONFIG = { skipTransform: true };
|
||||||
|
|
||||||
type Extents = {
|
type Extents = {
|
||||||
minX: number;
|
minX: number;
|
||||||
minY: number;
|
minY: number;
|
||||||
@ -54,10 +56,9 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => {
|
|||||||
/**
|
/**
|
||||||
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
||||||
* @param layer The konva layer to get the bounding box of.
|
* @param layer The konva layer to get the bounding box of.
|
||||||
* @param filterChildren Optional filter function to exclude certain children from the bounding box calculation. Defaults to including all children.
|
|
||||||
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
||||||
*/
|
*/
|
||||||
export const getKonvaLayerBbox = (layer: KonvaLayerType, preview: boolean = false): IRect | null => {
|
export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = false): IRect | null => {
|
||||||
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
||||||
//
|
//
|
||||||
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
||||||
@ -82,6 +83,7 @@ export const getKonvaLayerBbox = (layer: KonvaLayerType, preview: boolean = fals
|
|||||||
for (const child of layerClone.getChildren()) {
|
for (const child of layerClone.getChildren()) {
|
||||||
if (child.name() === VECTOR_MASK_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.opacity(1);
|
||||||
child.cache();
|
child.cache();
|
||||||
} else {
|
} else {
|
||||||
// Filter out unwanted children.
|
// Filter out unwanted children.
|
||||||
@ -112,11 +114,21 @@ export const getKonvaLayerBbox = (layer: KonvaLayerType, preview: boolean = fals
|
|||||||
|
|
||||||
// Correct the bounding box to be relative to the layer's position.
|
// Correct the bounding box to be relative to the layer's position.
|
||||||
const correctedLayerBbox = {
|
const correctedLayerBbox = {
|
||||||
x: layerBbox.minX - stage.x() + layerRect.x - layer.x(),
|
x: layerBbox.minX - Math.floor(stage.x()) + layerRect.x - Math.floor(layer.x()),
|
||||||
y: layerBbox.minY - stage.y() + layerRect.y - layer.y(),
|
y: layerBbox.minY - Math.floor(stage.y()) + layerRect.y - Math.floor(layer.y()),
|
||||||
width: layerBbox.maxX - layerBbox.minX,
|
width: layerBbox.maxX - layerBbox.minX,
|
||||||
height: layerBbox.maxY - layerBbox.minY,
|
height: layerBbox.maxY - layerBbox.minY,
|
||||||
};
|
};
|
||||||
|
|
||||||
return correctedLayerBbox;
|
return correctedLayerBbox;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getLayerBboxFast = (layer: KonvaLayerType): IRect | null => {
|
||||||
|
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
||||||
|
return {
|
||||||
|
x: Math.floor(bbox.x),
|
||||||
|
y: Math.floor(bbox.y),
|
||||||
|
width: Math.floor(bbox.width),
|
||||||
|
height: Math.floor(bbox.height),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -20,7 +20,7 @@ export const getRegionalPromptLayerBlobs = async (
|
|||||||
const reduxLayers = state.regionalPrompts.present.layers;
|
const reduxLayers = state.regionalPrompts.present.layers;
|
||||||
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, 'brush');
|
renderLayers(stage, reduxLayers, 1, 'brush');
|
||||||
|
|
||||||
const konvaLayers = stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`);
|
const konvaLayers = stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`);
|
||||||
const blobs: Record<string, Blob> = {};
|
const blobs: Record<string, Blob> = {};
|
||||||
@ -52,7 +52,7 @@ export const getRegionalPromptLayerBlobs = async (
|
|||||||
openBase64ImageInTab([
|
openBase64ImageInTab([
|
||||||
{
|
{
|
||||||
base64,
|
base64,
|
||||||
caption: `${reduxLayer.id}: ${reduxLayer.textPrompt?.positive} / ${reduxLayer.textPrompt?.negative}`,
|
caption: `${reduxLayer.id}: ${reduxLayer.positivePrompt} / ${reduxLayer.negativePrompt}`,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbaColorToString,rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import { getScaledFlooredCursorPosition } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
import { getScaledFlooredCursorPosition } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
||||||
import type { Layer, Tool, VectorMaskLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import type { Layer, Tool, VectorMaskLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import {
|
import {
|
||||||
@ -20,7 +20,7 @@ import {
|
|||||||
VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
||||||
VECTOR_MASK_LAYER_RECT_NAME,
|
VECTOR_MASK_LAYER_RECT_NAME,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { getKonvaLayerBbox } from 'features/regionalPrompts/util/bbox';
|
import { getLayerBboxFast, getLayerBboxPixels } from 'features/regionalPrompts/util/bbox';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
@ -33,8 +33,6 @@ const BBOX_NOT_SELECTED_MOUSEOVER_STROKE = 'rgba(255, 255, 255, 0.661)';
|
|||||||
const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
|
const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
|
||||||
const BRUSH_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 mapId = (object: { id: string }) => object.id;
|
const mapId = (object: { id: string }) => object.id;
|
||||||
|
|
||||||
const getIsSelected = (layerId?: string | null) => {
|
const getIsSelected = (layerId?: string | null) => {
|
||||||
@ -61,6 +59,7 @@ export const renderToolPreview = (
|
|||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
color: RgbColor | null,
|
color: RgbColor | null,
|
||||||
|
globalMaskLayerOpacity: number,
|
||||||
cursorPos: Vector2d | null,
|
cursorPos: Vector2d | null,
|
||||||
lastMouseDownPos: Vector2d | null,
|
lastMouseDownPos: Vector2d | null,
|
||||||
brushSize: number
|
brushSize: number
|
||||||
@ -161,7 +160,7 @@ export const renderToolPreview = (
|
|||||||
x: cursorPos.x,
|
x: cursorPos.x,
|
||||||
y: cursorPos.y,
|
y: cursorPos.y,
|
||||||
radius: brushSize / 2,
|
radius: brushSize / 2,
|
||||||
fill: rgbColorToString(color),
|
fill: rgbaColorToString({ ...color, a: globalMaskLayerOpacity }),
|
||||||
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -200,6 +199,7 @@ const renderVectorMaskLayer = (
|
|||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
vmLayer: VectorMaskLayer,
|
vmLayer: VectorMaskLayer,
|
||||||
vmLayerIndex: number,
|
vmLayerIndex: number,
|
||||||
|
globalMaskLayerOpacity: number,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
) => {
|
) => {
|
||||||
@ -354,8 +354,8 @@ const renderVectorMaskLayer = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Updating group opacity does not require re-caching
|
// Updating group opacity does not require re-caching
|
||||||
if (konvaObjectGroup.opacity() !== vmLayer.previewColor.a) {
|
if (konvaObjectGroup.opacity() !== globalMaskLayerOpacity) {
|
||||||
konvaObjectGroup.opacity(vmLayer.previewColor.a);
|
konvaObjectGroup.opacity(globalMaskLayerOpacity);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -370,6 +370,7 @@ const renderVectorMaskLayer = (
|
|||||||
export const renderLayers = (
|
export const renderLayers = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
reduxLayers: Layer[],
|
reduxLayers: Layer[],
|
||||||
|
globalMaskLayerOpacity: number,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
) => {
|
) => {
|
||||||
@ -386,7 +387,7 @@ export const renderLayers = (
|
|||||||
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 (isVectorMaskLayer(reduxLayer)) {
|
if (isVectorMaskLayer(reduxLayer)) {
|
||||||
renderVectorMaskLayer(stage, reduxLayer, layerIndex, tool, onLayerPosChanged);
|
renderVectorMaskLayer(stage, reduxLayer, layerIndex, globalMaskLayerOpacity, tool, onLayerPosChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -426,9 +427,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.needsPixelBbox
|
bbox = reduxLayer.needsPixelBbox ? getLayerBboxPixels(konvaLayer) : getLayerBboxFast(konvaLayer);
|
||||||
? getKonvaLayerBbox(konvaLayer)
|
|
||||||
: konvaLayer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
|
||||||
|
|
||||||
// Update the layer's bbox in the redux store
|
// Update the layer's bbox in the redux store
|
||||||
onBboxChanged(reduxLayer.id, bbox);
|
onBboxChanged(reduxLayer.id, bbox);
|
||||||
|
@ -13,51 +13,60 @@ import {
|
|||||||
selectValidIPAdapters,
|
selectValidIPAdapters,
|
||||||
selectValidT2IAdapters,
|
selectValidT2IAdapters,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||||
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { Fragment, memo } from 'react';
|
import { Fragment, memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => {
|
const selector = createMemoizedSelector(
|
||||||
const badges: string[] = [];
|
[selectControlAdaptersSlice, selectRegionalPromptsSlice],
|
||||||
let isError = false;
|
(controlAdapters, regionalPrompts) => {
|
||||||
|
const badges: string[] = [];
|
||||||
|
let isError = false;
|
||||||
|
|
||||||
const enabledIPAdapterCount = selectAllIPAdapters(controlAdapters).filter((ca) => ca.isEnabled).length;
|
const enabledIPAdapterCount = selectAllIPAdapters(controlAdapters)
|
||||||
const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length;
|
.filter((ca) => !regionalPrompts.present.layers.some((l) => l.ipAdapterIds.includes(ca.id)))
|
||||||
if (enabledIPAdapterCount > 0) {
|
.filter((ca) => ca.isEnabled).length;
|
||||||
badges.push(`${enabledIPAdapterCount} IP`);
|
|
||||||
}
|
|
||||||
if (enabledIPAdapterCount > validIPAdapterCount) {
|
|
||||||
isError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabledControlNetCount = selectAllControlNets(controlAdapters).filter((ca) => ca.isEnabled).length;
|
const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length;
|
||||||
const validControlNetCount = selectValidControlNets(controlAdapters).length;
|
if (enabledIPAdapterCount > 0) {
|
||||||
if (enabledControlNetCount > 0) {
|
badges.push(`${enabledIPAdapterCount} IP`);
|
||||||
badges.push(`${enabledControlNetCount} ControlNet`);
|
}
|
||||||
}
|
if (enabledIPAdapterCount > validIPAdapterCount) {
|
||||||
if (enabledControlNetCount > validControlNetCount) {
|
isError = true;
|
||||||
isError = true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters).filter((ca) => ca.isEnabled).length;
|
const enabledControlNetCount = selectAllControlNets(controlAdapters).filter((ca) => ca.isEnabled).length;
|
||||||
const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length;
|
const validControlNetCount = selectValidControlNets(controlAdapters).length;
|
||||||
if (enabledT2IAdapterCount > 0) {
|
if (enabledControlNetCount > 0) {
|
||||||
badges.push(`${enabledT2IAdapterCount} T2I`);
|
badges.push(`${enabledControlNetCount} ControlNet`);
|
||||||
}
|
}
|
||||||
if (enabledT2IAdapterCount > validT2IAdapterCount) {
|
if (enabledControlNetCount > validControlNetCount) {
|
||||||
isError = true;
|
isError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const controlAdapterIds = selectControlAdapterIds(controlAdapters);
|
const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters).filter((ca) => ca.isEnabled).length;
|
||||||
|
const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length;
|
||||||
|
if (enabledT2IAdapterCount > 0) {
|
||||||
|
badges.push(`${enabledT2IAdapterCount} T2I`);
|
||||||
|
}
|
||||||
|
if (enabledT2IAdapterCount > validT2IAdapterCount) {
|
||||||
|
isError = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const controlAdapterIds = selectControlAdapterIds(controlAdapters).filter(
|
||||||
controlAdapterIds,
|
(id) => !regionalPrompts.present.layers.some((l) => l.ipAdapterIds.includes(id))
|
||||||
badges,
|
);
|
||||||
isError, // TODO: Add some visual indicator that the control adapters are in an error state
|
|
||||||
};
|
return {
|
||||||
});
|
controlAdapterIds,
|
||||||
|
badges,
|
||||||
|
isError, // TODO: Add some visual indicator that the control adapters are in an error state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const ControlSettingsAccordion: React.FC = memo(() => {
|
export const ControlSettingsAccordion: React.FC = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -14,8 +14,8 @@ const selectValidLayerCount = createSelector(selectRegionalPromptsSlice, (region
|
|||||||
const validLayers = regionalPrompts.present.layers
|
const validLayers = regionalPrompts.present.layers
|
||||||
.filter((l) => l.isVisible)
|
.filter((l) => l.isVisible)
|
||||||
.filter((l) => {
|
.filter((l) => {
|
||||||
const hasTextPrompt = l.textPrompt && (l.textPrompt.positive || l.textPrompt.negative);
|
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
||||||
const hasAtLeastOneImagePrompt = l.imagePrompts.length > 0;
|
const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0;
|
||||||
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user