From 2f9a064d48e1816275514728c505fb0b25eba1fd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 9 May 2024 12:46:22 +1000 Subject: [PATCH] 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. --- .../components/CALayer/CALayer.tsx | 1 - .../components/IPALayer/IPALayer.tsx | 11 ++++-- .../controlLayers/store/controlLayersSlice.ts | 35 +++++++------------ .../src/features/controlLayers/store/types.ts | 4 +-- .../features/controlLayers/util/renderers.ts | 1 + .../web/src/features/metadata/util/parsers.ts | 3 +- 6 files changed, 27 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 984331a050..36509ec1d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -19,7 +19,6 @@ export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); const onClick = useCallback(() => { - // Must be capture so that the layer is selected before deleting/resetting/etc dispatch(layerSelected(layerId)); }, [dispatch, layerId]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index 02a161608d..2077700104 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -1,19 +1,26 @@ 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 { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; 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 = { layerId: string; }; 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 onClick = useCallback(() => { + dispatch(layerSelected(layerId)); + }, [dispatch, layerId]); return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index b191296ec0..425599561a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -124,11 +124,11 @@ const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; return LayerColors.next(lastColor); }; -const deselectAllLayers = (state: ControlLayersState) => { - for (const layer of state.layers.filter(isRenderableLayer)) { - layer.isSelected = false; +const exclusivelySelectLayer = (state: ControlLayersState, layerId: string) => { + for (const layer of state.layers) { + layer.isSelected = layer.id === layerId; } - state.selectedLayerId = null; + state.selectedLayerId = layerId; }; export const controlLayersSlice = createSlice({ @@ -137,12 +137,7 @@ export const controlLayersSlice = createSlice({ reducers: { //#region Any Layer Type layerSelected: (state, action: PayloadAction) => { - deselectAllLayers(state); - const layer = state.layers.find((l) => l.id === action.payload); - if (isRenderableLayer(layer)) { - layer.isSelected = true; - state.selectedLayerId = layer.id; - } + exclusivelySelectLayer(state, action.payload); }, layerVisibilityToggled: (state, action: PayloadAction) => { 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 }> ) => { const { layerId, controlAdapter } = action.payload; - deselectAllLayers(state); const layer: ControlAdapterLayer = { id: getCALayerId(layerId), type: 'control_adapter_layer', @@ -247,16 +241,15 @@ export const controlLayersSlice = createSlice({ controlAdapter, }; state.layers.push(layer); - state.selectedLayerId = layer.id; + exclusivelySelectLayer(state, layer.id); }, prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ payload: { layerId: uuidv4(), controlAdapter }, }), }, caLayerRecalled: (state, action: PayloadAction) => { - deselectAllLayers(state); 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 }>) => { const { layerId, imageDTO } = action.payload; @@ -359,9 +352,11 @@ export const controlLayersSlice = createSlice({ id: getIPALayerId(layerId), type: 'ip_adapter_layer', isEnabled: true, + isSelected: true, ipAdapter, }; state.layers.push(layer); + exclusivelySelectLayer(state, layer.id); }, prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), }, @@ -431,7 +426,6 @@ export const controlLayersSlice = createSlice({ rgLayerAdded: { reducer: (state, action: PayloadAction<{ layerId: string }>) => { const { layerId } = action.payload; - deselectAllLayers(state); const layer: RegionalGuidanceLayer = { id: getRGLayerId(layerId), type: 'regional_guidance_layer', @@ -450,14 +444,13 @@ export const controlLayersSlice = createSlice({ uploadedMaskImage: null, }; state.layers.push(layer); - state.selectedLayerId = layer.id; + exclusivelySelectLayer(state, layer.id); }, prepare: () => ({ payload: { layerId: uuidv4() } }), }, rgLayerRecalled: (state, action: PayloadAction) => { - deselectAllLayers(state); 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 }>) => { const { layerId, prompt } = action.payload; @@ -622,7 +615,6 @@ export const controlLayersSlice = createSlice({ //#region Initial Image Layer iiLayerAdded: { reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - deselectAllLayers(state); const { layerId, imageDTO } = action.payload; // Highlander! There can be only one! state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); @@ -640,15 +632,14 @@ export const controlLayersSlice = createSlice({ denoisingStrength: 0.75, }; state.layers.push(layer); - state.selectedLayerId = layer.id; + exclusivelySelectLayer(state, layer.id); }, prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }), }, iiLayerRecalled: (state, action: PayloadAction) => { - deselectAllLayers(state); state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : 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 }>) => { const { layerId, imageDTO } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 037034c33c..2d29b349cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -48,7 +48,8 @@ export type VectorMaskRect = z.infer; const zLayerBase = z.object({ id: z.string(), - isEnabled: z.boolean(), + isEnabled: z.boolean().default(true), + isSelected: z.boolean().default(true), }); const zRect = z.object({ @@ -62,7 +63,6 @@ const zRenderableLayerBase = zLayerBase.extend({ y: z.number(), bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), - isSelected: z.boolean(), }); const zControlAdapterLayer = zRenderableLayerBase.extend({ diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index f4931a7f9e..b74fef80c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -702,6 +702,7 @@ const renderLayers = ( if (isInitialImageLayer(reduxLayer)) { renderInitialImageLayer(stage, reduxLayer); } + // IP Adapter layers are not rendered } }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index eb53031578..ac42142ea2 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -692,8 +692,9 @@ const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async const layer: IPAdapterLayer = { id: getIPALayerId(uuidv4()), - isEnabled: true, type: 'ip_adapter_layer', + isEnabled: true, + isSelected: true, ipAdapter: { id: uuidv4(), type: 'ip_adapter',