feat(ui): ip adapter layers are selectable

This is largely an internal change, and it should have been this way from the start - less tip-toeing around layer types. The user-facing change is when you click an IP Adapter layer, it is highlighted. That's it.
This commit is contained in:
psychedelicious 2024-05-09 12:46:22 +10:00 committed by Kent Keirsey
parent b180666497
commit 2f9a064d48
6 changed files with 27 additions and 28 deletions

View File

@ -19,7 +19,6 @@ export const CALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected);
const onClick = useCallback(() => { const onClick = useCallback(() => {
// Must be capture so that the layer is selected before deleting/resetting/etc
dispatch(layerSelected(layerId)); dispatch(layerSelected(layerId));
}, [dispatch, layerId]); }, [dispatch, layerId]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });

View File

@ -1,19 +1,26 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper'; import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { memo } from 'react'; import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
type Props = { type Props = {
layerId: string; layerId: string;
}; };
export const IPALayer = memo(({ layerId }: Props) => { export const IPALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).isSelected);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onClick = useCallback(() => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
return ( return (
<LayerWrapper borderColor="base.800"> <LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} /> <LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="ip_adapter_layer" /> <LayerTitle type="ip_adapter_layer" />

View File

@ -124,11 +124,11 @@ const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => {
const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; const lastColor = rgLayers[rgLayers.length - 1]?.previewColor;
return LayerColors.next(lastColor); return LayerColors.next(lastColor);
}; };
const deselectAllLayers = (state: ControlLayersState) => { const exclusivelySelectLayer = (state: ControlLayersState, layerId: string) => {
for (const layer of state.layers.filter(isRenderableLayer)) { for (const layer of state.layers) {
layer.isSelected = false; layer.isSelected = layer.id === layerId;
} }
state.selectedLayerId = null; state.selectedLayerId = layerId;
}; };
export const controlLayersSlice = createSlice({ export const controlLayersSlice = createSlice({
@ -137,12 +137,7 @@ export const controlLayersSlice = createSlice({
reducers: { reducers: {
//#region Any Layer Type //#region Any Layer Type
layerSelected: (state, action: PayloadAction<string>) => { layerSelected: (state, action: PayloadAction<string>) => {
deselectAllLayers(state); exclusivelySelectLayer(state, action.payload);
const layer = state.layers.find((l) => l.id === action.payload);
if (isRenderableLayer(layer)) {
layer.isSelected = true;
state.selectedLayerId = layer.id;
}
}, },
layerVisibilityToggled: (state, action: PayloadAction<string>) => { layerVisibilityToggled: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload); const layer = state.layers.find((l) => l.id === action.payload);
@ -232,7 +227,6 @@ export const controlLayersSlice = createSlice({
action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }> action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }>
) => { ) => {
const { layerId, controlAdapter } = action.payload; const { layerId, controlAdapter } = action.payload;
deselectAllLayers(state);
const layer: ControlAdapterLayer = { const layer: ControlAdapterLayer = {
id: getCALayerId(layerId), id: getCALayerId(layerId),
type: 'control_adapter_layer', type: 'control_adapter_layer',
@ -247,16 +241,15 @@ export const controlLayersSlice = createSlice({
controlAdapter, controlAdapter,
}; };
state.layers.push(layer); state.layers.push(layer);
state.selectedLayerId = layer.id; exclusivelySelectLayer(state, layer.id);
}, },
prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({
payload: { layerId: uuidv4(), controlAdapter }, payload: { layerId: uuidv4(), controlAdapter },
}), }),
}, },
caLayerRecalled: (state, action: PayloadAction<ControlAdapterLayer>) => { caLayerRecalled: (state, action: PayloadAction<ControlAdapterLayer>) => {
deselectAllLayers(state);
state.layers.push({ ...action.payload, isSelected: true }); state.layers.push({ ...action.payload, isSelected: true });
state.selectedLayerId = action.payload.id; exclusivelySelectLayer(state, action.payload.id);
}, },
caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
const { layerId, imageDTO } = action.payload; const { layerId, imageDTO } = action.payload;
@ -359,9 +352,11 @@ export const controlLayersSlice = createSlice({
id: getIPALayerId(layerId), id: getIPALayerId(layerId),
type: 'ip_adapter_layer', type: 'ip_adapter_layer',
isEnabled: true, isEnabled: true,
isSelected: true,
ipAdapter, ipAdapter,
}; };
state.layers.push(layer); state.layers.push(layer);
exclusivelySelectLayer(state, layer.id);
}, },
prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }),
}, },
@ -431,7 +426,6 @@ export const controlLayersSlice = createSlice({
rgLayerAdded: { rgLayerAdded: {
reducer: (state, action: PayloadAction<{ layerId: string }>) => { reducer: (state, action: PayloadAction<{ layerId: string }>) => {
const { layerId } = action.payload; const { layerId } = action.payload;
deselectAllLayers(state);
const layer: RegionalGuidanceLayer = { const layer: RegionalGuidanceLayer = {
id: getRGLayerId(layerId), id: getRGLayerId(layerId),
type: 'regional_guidance_layer', type: 'regional_guidance_layer',
@ -450,14 +444,13 @@ export const controlLayersSlice = createSlice({
uploadedMaskImage: null, uploadedMaskImage: null,
}; };
state.layers.push(layer); state.layers.push(layer);
state.selectedLayerId = layer.id; exclusivelySelectLayer(state, layer.id);
}, },
prepare: () => ({ payload: { layerId: uuidv4() } }), prepare: () => ({ payload: { layerId: uuidv4() } }),
}, },
rgLayerRecalled: (state, action: PayloadAction<RegionalGuidanceLayer>) => { rgLayerRecalled: (state, action: PayloadAction<RegionalGuidanceLayer>) => {
deselectAllLayers(state);
state.layers.push({ ...action.payload, isSelected: true }); state.layers.push({ ...action.payload, isSelected: true });
state.selectedLayerId = action.payload.id; exclusivelySelectLayer(state, action.payload.id);
}, },
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
const { layerId, prompt } = action.payload; const { layerId, prompt } = action.payload;
@ -622,7 +615,6 @@ export const controlLayersSlice = createSlice({
//#region Initial Image Layer //#region Initial Image Layer
iiLayerAdded: { iiLayerAdded: {
reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
deselectAllLayers(state);
const { layerId, imageDTO } = action.payload; const { layerId, imageDTO } = action.payload;
// Highlander! There can be only one! // Highlander! There can be only one!
state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true));
@ -640,15 +632,14 @@ export const controlLayersSlice = createSlice({
denoisingStrength: 0.75, denoisingStrength: 0.75,
}; };
state.layers.push(layer); state.layers.push(layer);
state.selectedLayerId = layer.id; exclusivelySelectLayer(state, layer.id);
}, },
prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }), prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }),
}, },
iiLayerRecalled: (state, action: PayloadAction<InitialImageLayer>) => { iiLayerRecalled: (state, action: PayloadAction<InitialImageLayer>) => {
deselectAllLayers(state);
state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true));
state.layers.push({ ...action.payload, isSelected: true }); state.layers.push({ ...action.payload, isSelected: true });
state.selectedLayerId = action.payload.id; exclusivelySelectLayer(state, action.payload.id);
}, },
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
const { layerId, imageDTO } = action.payload; const { layerId, imageDTO } = action.payload;

View File

@ -48,7 +48,8 @@ export type VectorMaskRect = z.infer<typeof zVectorMaskRect>;
const zLayerBase = z.object({ const zLayerBase = z.object({
id: z.string(), id: z.string(),
isEnabled: z.boolean(), isEnabled: z.boolean().default(true),
isSelected: z.boolean().default(true),
}); });
const zRect = z.object({ const zRect = z.object({
@ -62,7 +63,6 @@ const zRenderableLayerBase = zLayerBase.extend({
y: z.number(), y: z.number(),
bbox: zRect.nullable(), bbox: zRect.nullable(),
bboxNeedsUpdate: z.boolean(), bboxNeedsUpdate: z.boolean(),
isSelected: z.boolean(),
}); });
const zControlAdapterLayer = zRenderableLayerBase.extend({ const zControlAdapterLayer = zRenderableLayerBase.extend({

View File

@ -702,6 +702,7 @@ const renderLayers = (
if (isInitialImageLayer(reduxLayer)) { if (isInitialImageLayer(reduxLayer)) {
renderInitialImageLayer(stage, reduxLayer); renderInitialImageLayer(stage, reduxLayer);
} }
// IP Adapter layers are not rendered
} }
}; };

View File

@ -692,8 +692,9 @@ const parseIPAdapterToIPAdapterLayer: MetadataParseFunc<IPAdapterLayer> = async
const layer: IPAdapterLayer = { const layer: IPAdapterLayer = {
id: getIPALayerId(uuidv4()), id: getIPALayerId(uuidv4()),
isEnabled: true,
type: 'ip_adapter_layer', type: 'ip_adapter_layer',
isEnabled: true,
isSelected: true,
ipAdapter: { ipAdapter: {
id: uuidv4(), id: uuidv4(),
type: 'ip_adapter', type: 'ip_adapter',