diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx index 2840bd3962..f58ba6770c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx @@ -13,17 +13,17 @@ type Props = { export const CAEntity = memo(({ id }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); - const disclosure = useDisclosure({ defaultIsOpen: true }); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const onClick = useCallback(() => { dispatch(entitySelected({ id, type: 'control_adapter' })); }, [dispatch, id]); return ( - + - {disclosure.isOpen && ( + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx index 67b2c539d5..3987787af7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx @@ -13,7 +13,7 @@ import { caMovedForwardOne, caMovedToBack, caMovedToFront, - selectCA, + selectCAOrThrow, selectControlAdaptersV2Slice, } from 'features/controlLayers/store/controlAdaptersSlice'; import { memo, useCallback } from 'react'; @@ -25,7 +25,6 @@ import { PiArrowUpBold, PiTrashSimpleBold, } from 'react-icons/pi'; -import { assert } from 'tsafe'; type Props = { id: string; @@ -34,8 +33,7 @@ type Props = { const selectValidActions = createAppSelector( [selectControlAdaptersV2Slice, (caState, id: string) => id], (caState, id) => { - const ca = selectCA(caState, id); - assert(ca, `CA with id ${id} not found`); + const ca = selectCAOrThrow(caState, id); const caIndex = caState.controlAdapters.indexOf(ca); const caCount = caState.controlAdapters.length; return { @@ -51,11 +49,7 @@ export const CAHeaderItems = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const validActions = useAppSelector((s) => selectValidActions(s, id)); - const isEnabled = useAppSelector((s) => { - const ca = selectCA(s.controlAdaptersV2, id); - assert(ca, `CA with id ${id} not found`); - return ca.isEnabled; - }); + const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled); const onToggle = useCallback(() => { dispatch(caIsEnabledToggled({ id })); }, [dispatch, id]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx index e2f009e48f..a668c90a81 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx @@ -1,14 +1,14 @@ -import { CannyProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor'; -import { ColorMapProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor'; -import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor'; -import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor'; -import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor'; -import { HedProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor'; -import { LineartProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor'; -import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor'; -import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor'; -import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor'; -import { PidiProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor'; +import { CannyProcessor } from 'features/controlLayers/components/ControlAdapter/processors/CannyProcessor'; +import { ColorMapProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor'; +import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor'; +import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor'; +import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor'; +import { HedProcessor } from 'features/controlLayers/components/ControlAdapter/processors/HedProcessor'; +import { LineartProcessor } from 'features/controlLayers/components/ControlAdapter/processors/LineartProcessor'; +import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor'; +import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor'; +import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor'; +import { PidiProcessor } from 'features/controlLayers/components/ControlAdapter/processors/PidiProcessor'; import type { ProcessorConfig } from 'features/controlLayers/store/types'; import { memo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx index d892070158..e20b79b8d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -1,7 +1,7 @@ import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { BeginEndStepPct } from 'features/controlLayers/components/Common/BeginEndStepPct'; -import { Weight } from 'features/controlLayers/components/Common/Weight'; +import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { Weight } from 'features/controlLayers/components/common/Weight'; import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect'; import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview'; import { CAModelCombobox } from 'features/controlLayers/components/ControlAdapter/CAModelCombobox'; @@ -15,6 +15,7 @@ import { caProcessedImageChanged, caProcessorConfigChanged, caWeightChanged, + selectCAOrThrow, } from 'features/controlLayers/store/controlAdaptersSlice'; import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/store/types'; import type { CAImageDropData } from 'features/dnd/types'; @@ -28,7 +29,6 @@ import type { ImageDTO, T2IAdapterModelConfig, } from 'services/api/types'; -import { assert } from 'tsafe'; type Props = { id: string; @@ -39,11 +39,7 @@ export const CASettings = memo(({ id }: Props) => { const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); - const controlAdapter = useAppSelector((s) => { - const ca = s.controlAdaptersV2.controlAdapters.find((ca) => ca.id === id); - assert(ca, `ControlAdapter with id ${id} not found`); - return ca; - }); + const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx index ef6e4160d6..d05d189712 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx @@ -1,6 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CA_PROCESSOR_DATA, type CannyProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { CannyProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx index 6faa00dd14..951e4c36db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx @@ -1,6 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CA_PROCESSOR_DATA, type ColorMapProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { ColorMapProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx index c03efd27c6..1b7b173287 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { ContentShuffleProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx index 3bbe813dcc..1e157adb2a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx @@ -1,7 +1,7 @@ import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { DWOpenposeProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx index 3cf61581ea..b4aa76ed6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx @@ -1,8 +1,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/store/types'; +import { isDepthAnythingModelSize } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx index 1ca75eae2f..6c27e386c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { HedProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { HedProcessorConfig } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx index aeb4121a36..4abf7e920c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { LineartProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { LineartProcessorConfig } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx index 0f45d83ef0..2cde63791e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx @@ -1,6 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CA_PROCESSOR_DATA, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { MediapipeFaceProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx index 1ce728984c..4f66f31a7f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { MidasDepthProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx index b6eef311ef..d578fc8ef3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { MlsdProcessorConfig } from 'features/controlLayers/store/types'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx index e7d559a1b4..6605baaadf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import type { PidiProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types'; +import type { PidiProcessorConfig } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts index 48a0942678..a9667437a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts @@ -1,4 +1,4 @@ -import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ProcessorConfig } from 'features/controlLayers/store/types'; export type ProcessorComponentProps = { onChange: (config: T) => void; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx deleted file mode 100644 index 75a1fa0c6b..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { BeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct'; -import { ControlAdapterWeight } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight'; -import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview'; -import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod'; -import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect'; -import type { CLIPVisionModelV2, IPAdapterConfigV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import type { TypesafeDroppableData } from 'features/dnd/types'; -import { memo } from 'react'; -import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types'; - -type Props = { - ipAdapter: IPAdapterConfigV2; - onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; - onChangeWeight: (weight: number) => void; - onChangeIPMethod: (method: IPMethodV2) => void; - onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; - onChangeImage: (imageDTO: ImageDTO | null) => void; - droppableData: TypesafeDroppableData; - postUploadAction: PostUploadAction; -}; - -export const IPAdapter = memo( - ({ - ipAdapter, - onChangeBeginEndStepPct, - onChangeWeight, - onChangeIPMethod, - onChangeModel, - onChangeCLIPVisionModel, - onChangeImage, - droppableData, - postUploadAction, - }: Props) => { - return ( - - - - - - - - - - - - - - - - - - ); - } -); - -IPAdapter.displayName = 'IPAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx deleted file mode 100644 index ed60951513..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; -import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import type { ImageDTO, PostUploadAction } from 'services/api/types'; - -type Props = { - image: ImageWithDims | null; - onChangeImage: (imageDTO: ImageDTO | null) => void; - ipAdapterId: string; // required for the dnd/upload interactions - droppableData: TypesafeDroppableData; - postUploadAction: PostUploadAction; -}; - -export const IPAdapterImagePreview = memo( - ({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); - const activeTabName = useAppSelector(activeTabNameSelector); - const optimalDimension = useAppSelector(selectOptimalDimension); - const shift = useShiftModifier(); - - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); - const handleResetControlImage = useCallback(() => { - onChangeImage(null); - }, [onChangeImage]); - - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } - - if (activeTabName === 'canvas') { - dispatch( - setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) - ); - } else { - const options = { updateAspectRatio: true, clamp: true }; - if (shift) { - const { width, height } = controlImage; - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } else { - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } - } - }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); - - const draggableData = useMemo(() => { - if (controlImage) { - return { - id: ipAdapterId, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, ipAdapterId]); - - useEffect(() => { - if (isConnected && isErrorControlImage) { - handleResetControlImage(); - } - }, [handleResetControlImage, isConnected, isErrorControlImage]); - - return ( - - - - {controlImage && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={ - shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') - } - /> - - )} - - ); - } -); - -IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx deleted file mode 100644 index b0541dca2c..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import type { CLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; -import { isCLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; -import type { AnyModelConfig, IPAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; - -const CLIP_VISION_OPTIONS = [ - { label: 'ViT-H', value: 'ViT-H' }, - { label: 'ViT-G', value: 'ViT-G' }, -]; - -type Props = { - modelKey: string | null; - onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - clipVisionModel: CLIPVisionModelV2; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; -}; - -export const IPAdapterModelSelect = memo( - ({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { - const { t } = useTranslation(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const [modelConfigs, { isLoading }] = useIPAdapterModels(); - const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); - - const _onChangeModel = useCallback( - (modelConfig: IPAdapterModelConfig | null) => { - if (!modelConfig) { - return; - } - onChangeModel(modelConfig); - }, - [onChangeModel] - ); - - const _onChangeCLIPVisionModel = useCallback( - (v) => { - assert(isCLIPVisionModelV2(v?.value)); - onChangeCLIPVisionModel(v.value); - }, - [onChangeCLIPVisionModel] - ); - - const getIsDisabled = useCallback( - (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base; - const hasMainModel = Boolean(currentBaseModel); - return !hasMainModel || !isCompatible; - }, - [currentBaseModel] - ); - - const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ - modelConfigs, - onChange: _onChangeModel, - selectedModel, - getIsDisabled, - isLoading, - }); - - const clipVisionModelValue = useMemo( - () => CLIP_VISION_OPTIONS.find((o) => o.value === clipVisionModel), - [clipVisionModel] - ); - - return ( - - - - - - - {selectedModel?.format === 'checkpoint' && ( - - - - )} - - ); - } -); - -IPAdapterModelSelect.displayName = 'IPAdapterModelSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx index 2aea3a422c..602b29914d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HeadsUpDisplay.tsx @@ -7,7 +7,6 @@ import { memo } from 'react'; export const HeadsUpDisplay = memo(() => { const stageAttrs = useStore($stageAttrs); - const layerCount = useAppSelector((s) => s.canvasV2.layers.length); const bbox = useAppSelector((s) => s.canvasV2.bbox); return ( @@ -15,7 +14,6 @@ export const HeadsUpDisplay = memo(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx deleted file mode 100644 index 7620fa57b6..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; -import { - caOrIPALayerBeginEndStepPctChanged, - caOrIPALayerWeightChanged, - ipAdapterCLIPVisionModelChanged, - ipAdapterImageChanged, - ipAdapterMethodChanged, - ipAdapterModelChanged, - selectLayerOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isIPAdapterLayer } from 'features/controlLayers/store/types'; -import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import type { IPAImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; -import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; - -type Props = { - layerId: string; -}; - -export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const ipAdapter = useAppSelector( - (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).ipAdapter - ); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch( - caOrIPALayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(caOrIPALayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - - const onChangeIPMethod = useCallback( - (method: IPMethodV2) => { - dispatch(ipAdapterMethodChanged({ layerId, method })); - }, - [dispatch, layerId] - ); - - const onChangeModel = useCallback( - (modelConfig: IPAdapterModelConfig) => { - dispatch(ipAdapterModelChanged({ layerId, modelConfig })); - }, - [dispatch, layerId] - ); - - const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModelV2) => { - dispatch(ipAdapterCLIPVisionModelChanged({ layerId, clipVisionModel })); - }, - [dispatch, layerId] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(ipAdapterImageChanged({ layerId, imageDTO })); - }, - [dispatch, layerId] - ); - - const droppableData = useMemo( - () => ({ - actionType: 'SET_IPA_LAYER_IMAGE', - context: { - layerId, - }, - id: layerId, - }), - [layerId] - ); - - const postUploadAction = useMemo( - () => ({ - type: 'SET_IPA_LAYER_IMAGE', - layerId, - }), - [layerId] - ); - - return ( - - ); -}); - -IPALayerIPAdapterWrapper.displayName = 'IPALayerIPAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx similarity index 61% rename from invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx index e4b89dfe21..f000045179 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx @@ -1,9 +1,7 @@ -import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { Flex, 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 { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { IPAHeaderItems } from 'features/controlLayers/components/IPAdapter/IPAHeaderItems'; +import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { entitySelected } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; @@ -23,14 +21,11 @@ export const IPAEntity = memo(({ id }: Props) => { return ( - - - - + {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx new file mode 100644 index 0000000000..d4de4f7f6f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx @@ -0,0 +1,39 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton'; +import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle'; +import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle'; +import { + ipaDeleted, + ipaIsEnabledToggled, + selectIPAOrThrow, +} from 'features/controlLayers/store/ipAdaptersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + id: string; +}; + +export const IPAHeaderItems = memo(({ id }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled); + const onToggle = useCallback(() => { + dispatch(ipaIsEnabledToggled({ id })); + }, [dispatch, id]); + const onDelete = useCallback(() => { + dispatch(ipaDeleted({ id })); + }, [dispatch, id]); + + return ( + <> + + + + + + ); +}); + +IPAHeaderItems.displayName = 'IPAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx new file mode 100644 index 0000000000..47fd0f5c92 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAImagePreview.tsx @@ -0,0 +1,100 @@ +import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/store/types'; +import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; +import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { memo, useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import type { ImageDTO, PostUploadAction } from 'services/api/types'; + +type Props = { + image: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + ipAdapterId: string; // required for the dnd/upload interactions + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isConnected = useAppSelector((s) => s.system.isConnected); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken); + const handleResetControlImage = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const handleSetControlImageToDimensions = useCallback(() => { + if (!controlImage) { + return; + } + + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } + }, [controlImage, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: ipAdapterId, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, ipAdapterId]); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + handleResetControlImage(); + } + }, [handleResetControlImage, isConnected, isErrorControlImage]); + + return ( + + + + {controlImage && ( + + } + tooltip={t('controlnet.resetControlImage')} + /> + } + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + /> + + )} + + ); +}); + +IPAImagePreview.displayName = 'IPAImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx index 4f6a468fc3..55c99fa6f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAMethod.tsx @@ -1,8 +1,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import { isIPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import type { IPMethodV2} from 'features/controlLayers/store/types'; +import { isIPMethodV2 } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; @@ -12,7 +12,7 @@ type Props = { onChange: (method: IPMethodV2) => void; }; -export const IPAdapterMethod = memo(({ method, onChange }: Props) => { +export const IPAMethod = memo(({ method, onChange }: Props) => { const { t } = useTranslation(); const options: { label: string; value: IPMethodV2 }[] = useMemo( () => [ @@ -41,4 +41,4 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => { ); }); -IPAdapterMethod.displayName = 'IPAdapterMethod'; +IPAMethod.displayName = 'IPAMethod'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx new file mode 100644 index 0000000000..08c3faeb2d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAModelCombobox.tsx @@ -0,0 +1,98 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import type { CLIPVisionModelV2} from 'features/controlLayers/store/types'; +import { isCLIPVisionModelV2 } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; +import type { AnyModelConfig, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +const CLIP_VISION_OPTIONS = [ + { label: 'ViT-H', value: 'ViT-H' }, + { label: 'ViT-G', value: 'ViT-G' }, +]; + +type Props = { + modelKey: string | null; + onChangeModel: (modelConfig: IPAdapterModelConfig) => void; + clipVisionModel: CLIPVisionModelV2; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; +}; + +export const IPAModelCombobox = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { + const { t } = useTranslation(); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs, { isLoading }] = useIPAdapterModels(); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); + + const _onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig | null) => { + if (!modelConfig) { + return; + } + onChangeModel(modelConfig); + }, + [onChangeModel] + ); + + const _onChangeCLIPVisionModel = useCallback( + (v) => { + assert(isCLIPVisionModelV2(v?.value)); + onChangeCLIPVisionModel(v.value); + }, + [onChangeCLIPVisionModel] + ); + + const getIsDisabled = useCallback( + (model: AnyModelConfig): boolean => { + const isCompatible = currentBaseModel === model.base; + const hasMainModel = Boolean(currentBaseModel); + return !hasMainModel || !isCompatible; + }, + [currentBaseModel] + ); + + const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ + modelConfigs, + onChange: _onChangeModel, + selectedModel, + getIsDisabled, + isLoading, + }); + + const clipVisionModelValue = useMemo( + () => CLIP_VISION_OPTIONS.find((o) => o.value === clipVisionModel), + [clipVisionModel] + ); + + return ( + + + + + + + {selectedModel?.format === 'checkpoint' && ( + + + + )} + + ); +}); + +IPAModelCombobox.displayName = 'IPAModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx new file mode 100644 index 0000000000..1daacb941f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx @@ -0,0 +1,122 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { Weight } from 'features/controlLayers/components/common/Weight'; +import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; +import { + ipaBeginEndStepPctChanged, + ipaCLIPVisionModelChanged, + ipaImageChanged, + ipaMethodChanged, + ipaModelChanged, + ipaWeightChanged, + selectIPAOrThrow, +} from 'features/controlLayers/store/ipAdaptersSlice'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; +import type { IPAImageDropData } from 'features/dnd/types'; +import { memo, useCallback, useMemo } from 'react'; +import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; + +import { IPAImagePreview } from './IPAImagePreview'; +import { IPAModelCombobox } from './IPAModelCombobox'; + +type Props = { + id: string; +}; + +export const IPASettings = memo(({ id }: Props) => { + const dispatch = useAppDispatch(); + const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id)); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch(ipaBeginEndStepPctChanged({ id, beginEndStepPct })); + }, + [dispatch, id] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(ipaWeightChanged({ id, weight })); + }, + [dispatch, id] + ); + + const onChangeIPMethod = useCallback( + (method: IPMethodV2) => { + dispatch(ipaMethodChanged({ id, method })); + }, + [dispatch, id] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(ipaModelChanged({ id, modelConfig })); + }, + [dispatch, id] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModelV2) => { + dispatch(ipaCLIPVisionModelChanged({ id, clipVisionModel })); + }, + [dispatch, id] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(ipaImageChanged({ id, imageDTO })); + }, + [dispatch, id] + ); + + const droppableData = useMemo( + () => ({ + actionType: 'SET_IPA_IMAGE', + context: { id }, + id, + }), + [id] + ); + + const postUploadAction = useMemo( + () => ({ + type: 'SET_IPA_IMAGE', + id, + }), + [id] + ); + + return ( + + + + + + + + + + + + + + + + + + ); +}); + +IPASettings.displayName = 'IPASettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index d9eadd1a31..6434ef402d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -1,14 +1,13 @@ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - $tool, - layerReset, - selectCanvasV2Slice, - selectedLayerDeleted, -} from 'features/controlLayers/store/controlLayersSlice'; -import { useCallback } from 'react'; +import { caDeleted } from 'features/controlLayers/store/controlAdaptersSlice'; +import { selectCanvasV2Slice, toolChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { ipaDeleted } from 'features/controlLayers/store/ipAdaptersSlice'; +import { layerDeleted, layerReset } from 'features/controlLayers/store/layersSlice'; +import { rgDeleted, rgReset } from 'features/controlLayers/store/regionalGuidanceSlice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { @@ -20,55 +19,94 @@ import { PiRectangleBold, } from 'react-icons/pi'; -const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => { - const selectedLayer = canvasV2.layers.find((l) => l.id === canvasV2.selectedLayerId); - return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; -}); +const DRAWING_TOOL_TYPES = ['layer', 'regional_guidance', 'inpaint_mask']; + +const getIsDrawingToolEnabled = (entityIdentifier: CanvasEntityIdentifier | null) => { + if (!entityIdentifier) { + return false; + } + return DRAWING_TOOL_TYPES.includes(entityIdentifier.type); +}; + +const selectSelectedEntityIdentifier = createMemoizedSelector( + selectCanvasV2Slice, + (canvasV2State) => canvasV2State.selectedEntityIdentifier +); export const ToolChooser: React.FC = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isDisabled = useAppSelector(selectIsDisabled); - const selectedLayerId = useAppSelector((s) => s.canvasV2.selectedLayerId); - const tool = useStore($tool); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const isDrawingToolDisabled = useMemo( + () => !getIsDrawingToolEnabled(selectedEntityIdentifier), + [selectedEntityIdentifier] + ); + const isMoveToolDisabled = useMemo(() => selectedEntityIdentifier === null, [selectedEntityIdentifier]); + const tool = useAppSelector((s) => s.canvasV2.tool.selected); const setToolToBrush = useCallback(() => { - $tool.set('brush'); - }, []); - useHotkeys('b', setToolToBrush, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('brush')); + }, [dispatch]); + useHotkeys('b', setToolToBrush, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToBrush]); const setToolToEraser = useCallback(() => { - $tool.set('eraser'); - }, []); - useHotkeys('e', setToolToEraser, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('eraser')); + }, [dispatch]); + useHotkeys('e', setToolToEraser, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToEraser]); const setToolToRect = useCallback(() => { - $tool.set('rect'); - }, []); - useHotkeys('u', setToolToRect, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('rect')); + }, [dispatch]); + useHotkeys('u', setToolToRect, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToRect]); const setToolToMove = useCallback(() => { - $tool.set('move'); - }, []); - useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('move')); + }, [dispatch]); + useHotkeys('v', setToolToMove, { enabled: !isMoveToolDisabled }, [isMoveToolDisabled, setToolToMove]); const setToolToView = useCallback(() => { - $tool.set('view'); - }, []); - useHotkeys('h', setToolToView, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('view')); + }, [dispatch]); + useHotkeys('h', setToolToView, [setToolToView]); const setToolToBbox = useCallback(() => { - $tool.set('bbox'); - }, []); - useHotkeys('q', setToolToBbox, { enabled: !isDisabled }, [isDisabled]); + dispatch(toolChanged('bbox')); + }, [dispatch]); + useHotkeys('q', setToolToBbox, [setToolToBbox]); const resetSelectedLayer = useCallback(() => { - if (selectedLayerId === null) { + if (selectedEntityIdentifier === null) { return; } - dispatch(layerReset(selectedLayerId)); - }, [dispatch, selectedLayerId]); - useHotkeys('shift+c', resetSelectedLayer); + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerReset({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgReset({ id })); + } + }, [dispatch, selectedEntityIdentifier]); + const isResetEnabled = useMemo( + () => selectedEntityIdentifier?.type === 'layer' || selectedEntityIdentifier?.type === 'regional_guidance', + [selectedEntityIdentifier] + ); + useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [isResetEnabled, resetSelectedLayer]); const deleteSelectedLayer = useCallback(() => { - dispatch(selectedLayerDeleted()); - }, [dispatch]); - useHotkeys('shift+d', deleteSelectedLayer); + if (selectedEntityIdentifier === null) { + return; + } + const { type, id } = selectedEntityIdentifier; + if (type === 'layer') { + dispatch(layerDeleted({ id })); + } + if (type === 'regional_guidance') { + dispatch(rgDeleted({ id })); + } + if (type === 'control_adapter') { + dispatch(caDeleted({ id })); + } + if (type === 'ip_adapter') { + dispatch(ipaDeleted({ id })); + } + }, [dispatch, selectedEntityIdentifier]); + const isDeleteEnabled = useMemo(() => selectedEntityIdentifier !== null, [selectedEntityIdentifier]); + useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]); return ( @@ -78,7 +116,7 @@ export const ToolChooser: React.FC = () => { icon={} variant={tool === 'brush' ? 'solid' : 'outline'} onClick={setToolToBrush} - isDisabled={isDisabled} + isDisabled={isDrawingToolDisabled} /> { icon={} variant={tool === 'eraser' ? 'solid' : 'outline'} onClick={setToolToEraser} - isDisabled={isDisabled} + isDisabled={isDrawingToolDisabled} /> { icon={} variant={tool === 'rect' ? 'solid' : 'outline'} onClick={setToolToRect} - isDisabled={isDisabled} + isDisabled={isDrawingToolDisabled} /> { icon={} variant={tool === 'move' ? 'solid' : 'outline'} onClick={setToolToMove} - isDisabled={isDisabled} + isDisabled={isMoveToolDisabled} /> { icon={} variant={tool === 'view' ? 'solid' : 'outline'} onClick={setToolToView} - isDisabled={isDisabled} /> { icon={} variant={tool === 'bbox' ? 'solid' : 'outline'} onClick={setToolToBbox} - isDisabled={isDisabled} /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/BeginEndStepPct.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/BeginEndStepPct.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/Weight.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/Weight.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts index 6a98523bef..8faa0a900b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts @@ -6,6 +6,7 @@ import { zModelIdentifierField } from 'features/nodes/types/common'; import type { IRect } from 'konva/lib/types'; import { isEqual } from 'lodash-es'; import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { ControlAdapterConfig, ControlAdapterData, ControlModeV2, Filter, ProcessorConfig } from './types'; @@ -22,6 +23,11 @@ const initialState: ControlAdaptersV2State = { }; export const selectCA = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); +export const selectCAOrThrow = (state: ControlAdaptersV2State, id: string) => { + const ca = selectCA(state, id); + assert(ca, `Control Adapter with id ${id} not found`); + return ca; +}; export const controlAdaptersV2Slice = createSlice({ name: 'controlAdaptersV2', diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts index 7424d41903..9e9a9541ae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts @@ -1,13 +1,13 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; -import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -import type { IPAdapterConfig, IPAdapterData } from './types'; +import type { CLIPVisionModelV2, IPAdapterConfig, IPAdapterData, IPMethodV2 } from './types'; +import { imageDTOToImageWithDims } from './types'; type IPAdaptersState = { _version: 1; @@ -19,7 +19,12 @@ const initialState: IPAdaptersState = { ipAdapters: [], }; -const selectIpa = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); +export const selectIPA = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); +export const selectIPAOrThrow = (state: IPAdaptersState, id: string) => { + const ipa = selectIPA(state, id); + assert(ipa, `IP Adapter with id ${id} not found`); + return ipa; +}; export const ipAdaptersSlice = createSlice({ name: 'ipAdapters', @@ -41,11 +46,11 @@ export const ipAdaptersSlice = createSlice({ ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { state.ipAdapters.push(action.payload.data); }, - ipaIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - const ipa = selectIpa(state, id); + ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ipa = selectIPA(state, id); if (ipa) { - ipa.isEnabled = isEnabled; + ipa.isEnabled = !ipa.isEnabled; } }, ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { @@ -53,7 +58,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { const { id, imageDTO } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -61,7 +66,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { const { id, method } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -75,7 +80,7 @@ export const ipAdaptersSlice = createSlice({ }> ) => { const { id, modelConfig } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -87,7 +92,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { const { id, clipVisionModel } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -95,7 +100,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { const { id, weight } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -103,7 +108,7 @@ export const ipAdaptersSlice = createSlice({ }, ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { const { id, beginEndStepPct } = action.payload; - const ipa = selectIpa(state, id); + const ipa = selectIPA(state, id); if (!ipa) { return; } @@ -115,7 +120,7 @@ export const ipAdaptersSlice = createSlice({ export const { ipaAdded, ipaRecalled, - ipaIsEnabledChanged, + ipaIsEnabledToggled, ipaDeleted, ipaImageChanged, ipaMethodChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index 182693abcc..cb41306b7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -87,6 +87,17 @@ export const regionalGuidanceSlice = createSlice({ }, prepare: () => ({ payload: { id: uuidv4() } }), }, + rgReset: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects = []; + rg.bbox = null; + rg.bboxNeedsUpdate = false; + rg.imageCache = null; + }, rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { const { data } = action.payload; state.regions.push(data); @@ -388,6 +399,7 @@ export const regionalGuidanceSlice = createSlice({ export const { rgAdded, rgRecalled, + rgReset, rgIsEnabledToggled, rgTranslated, rgBboxChanged, diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index c5bef7f6db..b78b643731 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -290,3 +290,14 @@ const InvokeTabs = () => { }; export default memo(InvokeTabs); + +const ParametersPanelComponent = memo(() => { + const activeTabName = useAppSelector(activeTabNameSelector); + + if (activeTabName === 'workflows') { + return ; + } else { + return ; + } +}); +ParametersPanelComponent.displayName = 'ParametersPanelComponent';