feat(ui): per-layer autonegative

This commit is contained in:
psychedelicious 2024-04-19 08:37:11 +10:00 committed by Kent Keirsey
parent d3aa97ab99
commit a6e64423d9
6 changed files with 79 additions and 56 deletions

View File

@ -22,7 +22,6 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
const { dispatch } = getStore(); const { dispatch } = getStore();
// TODO: Handle non-SDXL // TODO: Handle non-SDXL
// const isSDXL = state.generation.model?.base === 'sdxl'; // const isSDXL = state.generation.model?.base === 'sdxl';
const { autoNegative } = state.regionalPrompts.present;
const layers = state.regionalPrompts.present.layers const layers = state.regionalPrompts.present.layers
.filter((l) => l.kind === 'promptRegionLayer') // We only want the prompt region layers .filter((l) => l.kind === 'promptRegionLayer') // We only want the prompt region layers
.filter((l) => l.isVisible); // Only visible layers are rendered on the canvas .filter((l) => l.isVisible); // Only visible layers are rendered on the canvas
@ -205,7 +204,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 (autoNegative === 'invert') { if (layer.autoNegative === 'invert') {
// 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
// TODO: Probably faster to invert the tensor from the earlier mask rather than read the mask image and convert... // TODO: Probably faster to invert the tensor from the earlier mask rather than read the mask image and convert...
const invertedMaskToTensorNode: S['AlphaMaskToTensorInvocation'] = { const invertedMaskToTensorNode: S['AlphaMaskToTensorInvocation'] = {

View File

@ -0,0 +1,62 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import {
layerAutoNegativeChanged,
selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
const options: ComboboxOption[] = [
{ label: 'Off', value: 'off' },
{ label: 'Invert', value: 'invert' },
];
type Props = {
layerId: string;
};
const useAutoNegative = (layerId: string) => {
const selectAutoNegative = useMemo(
() =>
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return layer.autoNegative;
}),
[layerId]
);
const autoNegative = useAppSelector(selectAutoNegative);
return autoNegative;
};
const AutoNegativeCombobox = ({ layerId }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const autoNegative = useAutoNegative(layerId);
const onChange = useCallback<ComboboxOnChange>(
(v) => {
if (!isParameterAutoNegative(v?.value)) {
return;
}
dispatch(layerAutoNegativeChanged({ layerId, autoNegative: v.value }));
},
[dispatch, layerId]
);
const value = useMemo(() => options.find((o) => o.value === autoNegative), [autoNegative]);
return (
<FormControl flexGrow={0} gap={2} w="min-content">
<FormLabel m={0}>AutoNegative</FormLabel>
<Combobox value={value} options={options} onChange={onChange} isSearchable={false} sx={{ w: '5.2rem' }} />
</FormControl>
);
};
export default memo(AutoNegativeCombobox);

View File

@ -1,6 +1,7 @@
import { Flex, Spacer, Text } from '@invoke-ai/ui-library'; import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString } from 'features/canvas/util/colorToString';
import LayerAutoNegativeCombobox from 'features/regionalPrompts/components/LayerAutoNegativeCombobox';
import { LayerColorPicker } from 'features/regionalPrompts/components/LayerColorPicker'; import { LayerColorPicker } from 'features/regionalPrompts/components/LayerColorPicker';
import { LayerMenu } from 'features/regionalPrompts/components/LayerMenu'; import { LayerMenu } from 'features/regionalPrompts/components/LayerMenu';
import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/regionalPrompts/components/LayerVisibilityToggle';
@ -8,7 +9,6 @@ import { RegionalPromptsNegativePrompt } from 'features/regionalPrompts/componen
import { RegionalPromptsPositivePrompt } from 'features/regionalPrompts/components/RegionalPromptsPositivePrompt'; import { RegionalPromptsPositivePrompt } from 'features/regionalPrompts/components/RegionalPromptsPositivePrompt';
import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
id: string; id: string;
@ -16,7 +16,6 @@ type Props = {
export const LayerListItem = memo(({ id }: Props) => { export const LayerListItem = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const selectedLayer = useAppSelector((s) => s.regionalPrompts.present.selectedLayer); const selectedLayer = useAppSelector((s) => s.regionalPrompts.present.selectedLayer);
const color = useAppSelector((s) => { const color = useAppSelector((s) => {
const color = s.regionalPrompts.present.layers.find((l) => l.id === id)?.color; const color = s.regionalPrompts.present.layers.find((l) => l.id === id)?.color;
@ -36,11 +35,7 @@ export const LayerListItem = memo(({ id }: Props) => {
<LayerColorPicker id={id} /> <LayerColorPicker id={id} />
<LayerVisibilityToggle id={id} /> <LayerVisibilityToggle id={id} />
<Spacer /> <Spacer />
{selectedLayer === id && ( <LayerAutoNegativeCombobox layerId={id} />
<Text color="base.300" fontWeight="semibold" pe={2}>
{t('common.selected')}
</Text>
)}
<LayerMenu id={id} /> <LayerMenu id={id} />
</Flex> </Flex>
<RegionalPromptsPositivePrompt layerId={id} /> <RegionalPromptsPositivePrompt layerId={id} />

View File

@ -1,39 +0,0 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import { autoNegativeChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const options: ComboboxOption[] = [
{ label: 'Off', value: 'off' },
{ label: 'Invert', value: 'invert' },
];
const AutoNegativeCombobox = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const autoNegative = useAppSelector((s) => s.regionalPrompts.present.autoNegative);
const onChange = useCallback<ComboboxOnChange>(
(v) => {
if (!isParameterAutoNegative(v?.value)) {
return;
}
dispatch(autoNegativeChanged(v.value));
},
[dispatch]
);
const value = useMemo(() => options.find((o) => o.value === autoNegative), [autoNegative]);
return (
<FormControl>
<FormLabel>Negative Mode</FormLabel>
<Combobox value={value} options={options} onChange={onChange} isSearchable={false} />
</FormControl>
);
};
export default memo(AutoNegativeCombobox);

View File

@ -6,7 +6,6 @@ import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButt
import { BrushSize } from 'features/regionalPrompts/components/BrushSize'; import { BrushSize } from 'features/regionalPrompts/components/BrushSize';
import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton'; import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton';
import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem'; import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem';
import AutoNegativeCombobox from 'features/regionalPrompts/components/NegativeModeCombobox';
import { PromptLayerOpacity } from 'features/regionalPrompts/components/PromptLayerOpacity'; import { PromptLayerOpacity } from 'features/regionalPrompts/components/PromptLayerOpacity';
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';
@ -39,7 +38,6 @@ export const RegionalPromptsEditor = memo(() => {
</Flex> </Flex>
<BrushSize /> <BrushSize />
<PromptLayerOpacity /> <PromptLayerOpacity />
<AutoNegativeCombobox />
{layerIdsReversed.map((id) => ( {layerIdsReversed.map((id) => (
<LayerListItem key={id} id={id} /> <LayerListItem key={id} id={id} />
))} ))}

View File

@ -56,6 +56,7 @@ type PromptRegionLayer = LayerBase & {
positivePrompt: string; positivePrompt: string;
negativePrompt: string; negativePrompt: string;
color: RgbColor; color: RgbColor;
autoNegative: ParameterAutoNegative;
}; };
export type Layer = PromptRegionLayer; export type Layer = PromptRegionLayer;
@ -67,7 +68,6 @@ type RegionalPromptsState = {
layers: PromptRegionLayer[]; layers: PromptRegionLayer[];
brushSize: number; brushSize: number;
promptLayerOpacity: number; promptLayerOpacity: number;
autoNegative: ParameterAutoNegative;
}; };
export const initialRegionalPromptsState: RegionalPromptsState = { export const initialRegionalPromptsState: RegionalPromptsState = {
@ -77,7 +77,6 @@ export const initialRegionalPromptsState: RegionalPromptsState = {
brushSize: 40, brushSize: 40,
layers: [], layers: [],
promptLayerOpacity: 0.5, // This currently doesn't work promptLayerOpacity: 0.5, // This currently doesn't work
autoNegative: 'off',
}; };
const isLine = (obj: LayerObject): obj is LineObject => obj.kind === 'line'; const isLine = (obj: LayerObject): obj is LineObject => obj.kind === 'line';
@ -99,6 +98,7 @@ export const regionalPromptsSlice = createSlice({
color: action.meta.color, color: action.meta.color,
x: 0, x: 0,
y: 0, y: 0,
autoNegative: 'off',
}; };
state.layers.push(layer); state.layers.push(layer);
state.selectedLayer = layer.id; state.selectedLayer = layer.id;
@ -233,8 +233,16 @@ export const regionalPromptsSlice = createSlice({
promptLayerOpacityChanged: (state, action: PayloadAction<number>) => { promptLayerOpacityChanged: (state, action: PayloadAction<number>) => {
state.promptLayerOpacity = action.payload; state.promptLayerOpacity = action.payload;
}, },
autoNegativeChanged: (state, action: PayloadAction<ParameterAutoNegative>) => { layerAutoNegativeChanged: (
state.autoNegative = action.payload; state,
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
) => {
const { layerId, autoNegative } = action.payload;
const layer = state.layers.find((l) => l.id === layerId);
if (!layer || layer.kind !== 'promptRegionLayer') {
return;
}
layer.autoNegative = autoNegative;
}, },
lineFinished: (state) => { lineFinished: (state) => {
console.log('lineFinished'); console.log('lineFinished');
@ -288,7 +296,7 @@ export const {
layerBboxChanged, layerBboxChanged,
promptLayerOpacityChanged, promptLayerOpacityChanged,
allLayersDeleted, allLayersDeleted,
autoNegativeChanged, layerAutoNegativeChanged,
} = regionalPromptsSlice.actions; } = regionalPromptsSlice.actions;
export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts; export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts;