refactor(ui): canvas v2 (wip)

This commit is contained in:
psychedelicious 2024-06-14 21:59:20 +10:00
parent 8533f207dc
commit 28fc9a387c
34 changed files with 552 additions and 531 deletions

View File

@ -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 (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={disclosure.onToggle}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<CAHeaderItems id={id} />
</Flex>
{disclosure.isOpen && (
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CASettings id={id} />
</Flex>

View File

@ -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]);

View File

@ -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';

View File

@ -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]) => {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -1,4 +1,4 @@
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import type { ProcessorConfig } from 'features/controlLayers/store/types';
export type ProcessorComponentProps<T extends ProcessorConfig> = {
onChange: (config: T) => void;

View File

@ -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 (
<Flex flexDir="column" gap={4} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<IPAdapterModelSelect
modelKey={ipAdapter.model?.key ?? null}
onChangeModel={onChangeModel}
clipVisionModel={ipAdapter.clipVisionModel}
onChangeCLIPVisionModel={onChangeCLIPVisionModel}
/>
</Box>
</Flex>
<Flex gap={4} w="full" alignItems="center">
<Flex flexDir="column" gap={3} w="full">
<IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
<ControlAdapterWeight weight={ipAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct
beginEndStepPct={ipAdapter.beginEndStepPct}
onChange={onChangeBeginEndStepPct}
/>
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<IPAdapterImagePreview
image={ipAdapter.image}
onChangeImage={onChangeImage}
ipAdapterId={ipAdapter.id}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
</Flex>
</Flex>
);
}
);
IPAdapter.displayName = 'IPAdapter';

View File

@ -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<ImageDraggableData | undefined>(() => {
if (controlImage) {
return {
id: ipAdapterId,
payloadType: 'IMAGE_DTO',
payload: { imageDTO: controlImage },
};
}
}, [controlImage, ipAdapterId]);
useEffect(() => {
if (isConnected && isErrorControlImage) {
handleResetControlImage();
}
}, [handleResetControlImage, isConnected, isErrorControlImage]);
return (
<Flex position="relative" w={36} h={36} alignItems="center">
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={controlImage}
postUploadAction={postUploadAction}
/>
{controlImage && (
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={handleSetControlImageToDimensions}
icon={<PiRulerBold size={16} />}
tooltip={
shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')
}
/>
</Flex>
)}
</Flex>
);
}
);
IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';

View File

@ -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<ComboboxOnChange>(
(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 (
<Flex gap={4}>
<Tooltip label={selectedModel?.description}>
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full">
<Combobox
options={options}
placeholder={t('controlnet.selectModel')}
value={value}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
</Tooltip>
{selectedModel?.format === 'checkpoint' && (
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} width="max-content" minWidth={28}>
<Combobox
options={CLIP_VISION_OPTIONS}
placeholder={t('controlnet.selectCLIPVisionModel')}
value={clipVisionModelValue}
onChange={_onChangeCLIPVisionModel}
/>
</FormControl>
)}
</Flex>
);
}
);
IPAdapterModelSelect.displayName = 'IPAdapterModelSelect';

View File

@ -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(() => {
<HUDItem label="Scale" value={round(stageAttrs.scale, 3)} />
<HUDItem label="Stage Pos" value={`${round(stageAttrs.x, 3)}, ${round(stageAttrs.y, 3)}`} />
<HUDItem label="Stage Size" value={`${round(stageAttrs.width, 3)}, ${round(stageAttrs.height, 3)}`} />
<HUDItem label="Layer Count" value={layerCount} />
<HUDItem label="BBox Size" value={`${bbox.width}×${bbox.height}`} />
<HUDItem label="BBox Position" value={`${bbox.x}, ${bbox.y}`} />
<HUDItem label="BBox Width % 8" value={round(bbox.width % 8, 3)} />

View File

@ -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<IPAImageDropData>(
() => ({
actionType: 'SET_IPA_LAYER_IMAGE',
context: {
layerId,
},
id: layerId,
}),
[layerId]
);
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
() => ({
type: 'SET_IPA_LAYER_IMAGE',
layerId,
}),
[layerId]
);
return (
<IPAdapter
ipAdapter={ipAdapter}
onChangeBeginEndStepPct={onChangeBeginEndStepPct}
onChangeWeight={onChangeWeight}
onChangeIPMethod={onChangeIPMethod}
onChangeModel={onChangeModel}
onChangeCLIPVisionModel={onChangeCLIPVisionModel}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
);
});
IPALayerIPAdapterWrapper.displayName = 'IPALayerIPAdapterWrapper';

View File

@ -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 (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<EntityEnabledToggle id={id} />
<EntityTitle type="ip_adapter" />
<Spacer />
<LayerDeleteButton id={id} />
<IPAHeaderItems id={id} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<IPALayerIPAdapterWrapper id={id} />
<IPASettings id={id} />
</Flex>
)}
</LayerWrapper>

View File

@ -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 (
<>
<EntityEnabledToggle isEnabled={isEnabled} onToggle={onToggle} />
<EntityTitle title={t('controlLayers.ipAdapter')} />
<Spacer />
<EntityDeleteButton onDelete={onDelete} />
</>
);
});
IPAHeaderItems.displayName = 'IPAHeaderItems';

View File

@ -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<ImageDraggableData | undefined>(() => {
if (controlImage) {
return {
id: ipAdapterId,
payloadType: 'IMAGE_DTO',
payload: { imageDTO: controlImage },
};
}
}, [controlImage, ipAdapterId]);
useEffect(() => {
if (isConnected && isErrorControlImage) {
handleResetControlImage();
}
}, [handleResetControlImage, isConnected, isErrorControlImage]);
return (
<Flex position="relative" w={36} h={36} alignItems="center">
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={controlImage}
postUploadAction={postUploadAction}
/>
{controlImage && (
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={handleSetControlImageToDimensions}
icon={<PiRulerBold size={16} />}
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
/>
</Flex>
)}
</Flex>
);
});
IPAImagePreview.displayName = 'IPAImagePreview';

View File

@ -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';

View File

@ -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<ComboboxOnChange>(
(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 (
<Flex gap={4}>
<Tooltip label={selectedModel?.description}>
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full">
<Combobox
options={options}
placeholder={t('controlnet.selectModel')}
value={value}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
</Tooltip>
{selectedModel?.format === 'checkpoint' && (
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} width="max-content" minWidth={28}>
<Combobox
options={CLIP_VISION_OPTIONS}
placeholder={t('controlnet.selectCLIPVisionModel')}
value={clipVisionModelValue}
onChange={_onChangeCLIPVisionModel}
/>
</FormControl>
)}
</Flex>
);
});
IPAModelCombobox.displayName = 'IPAModelCombobox';

View File

@ -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<IPAImageDropData>(
() => ({
actionType: 'SET_IPA_IMAGE',
context: { id },
id,
}),
[id]
);
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
() => ({
type: 'SET_IPA_IMAGE',
id,
}),
[id]
);
return (
<Flex flexDir="column" gap={4} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<IPAModelCombobox
modelKey={ipAdapter.model?.key ?? null}
onChangeModel={onChangeModel}
clipVisionModel={ipAdapter.clipVisionModel}
onChangeCLIPVisionModel={onChangeCLIPVisionModel}
/>
</Box>
</Flex>
<Flex gap={4} w="full" alignItems="center">
<Flex flexDir="column" gap={3} w="full">
<IPAMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
<Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<IPAImagePreview
image={ipAdapter.image}
onChangeImage={onChangeImage}
ipAdapterId={ipAdapter.id}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
</Flex>
</Flex>
);
});
IPASettings.displayName = 'IPASettings';

View File

@ -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 (
<ButtonGroup isAttached>
@ -78,7 +116,7 @@ export const ToolChooser: React.FC = () => {
icon={<PiPaintBrushBold />}
variant={tool === 'brush' ? 'solid' : 'outline'}
onClick={setToolToBrush}
isDisabled={isDisabled}
isDisabled={isDrawingToolDisabled}
/>
<IconButton
aria-label={`${t('unifiedCanvas.eraser')} (E)`}
@ -86,7 +124,7 @@ export const ToolChooser: React.FC = () => {
icon={<PiEraserBold />}
variant={tool === 'eraser' ? 'solid' : 'outline'}
onClick={setToolToEraser}
isDisabled={isDisabled}
isDisabled={isDrawingToolDisabled}
/>
<IconButton
aria-label={`${t('controlLayers.rectangle')} (U)`}
@ -94,7 +132,7 @@ export const ToolChooser: React.FC = () => {
icon={<PiRectangleBold />}
variant={tool === 'rect' ? 'solid' : 'outline'}
onClick={setToolToRect}
isDisabled={isDisabled}
isDisabled={isDrawingToolDisabled}
/>
<IconButton
aria-label={`${t('unifiedCanvas.move')} (V)`}
@ -102,7 +140,7 @@ export const ToolChooser: React.FC = () => {
icon={<PiArrowsOutCardinalBold />}
variant={tool === 'move' ? 'solid' : 'outline'}
onClick={setToolToMove}
isDisabled={isDisabled}
isDisabled={isMoveToolDisabled}
/>
<IconButton
aria-label={`${t('unifiedCanvas.view')} (H)`}
@ -110,7 +148,6 @@ export const ToolChooser: React.FC = () => {
icon={<PiHandBold />}
variant={tool === 'view' ? 'solid' : 'outline'}
onClick={setToolToView}
isDisabled={isDisabled}
/>
<IconButton
aria-label={`${t('controlLayers.bbox')} (Q)`}
@ -118,7 +155,6 @@ export const ToolChooser: React.FC = () => {
icon={<PiBoundingBoxBold />}
variant={tool === 'bbox' ? 'solid' : 'outline'}
onClick={setToolToBbox}
isDisabled={isDisabled}
/>
</ButtonGroup>
);

View File

@ -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',

View File

@ -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,

View File

@ -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,

View File

@ -290,3 +290,14 @@ const InvokeTabs = () => {
};
export default memo(InvokeTabs);
const ParametersPanelComponent = memo(() => {
const activeTabName = useAppSelector(activeTabNameSelector);
if (activeTabName === 'workflows') {
return <NodeEditorPanelGroup />;
} else {
return <ParametersPanelTextToImage />;
}
});
ParametersPanelComponent.displayName = 'ParametersPanelComponent';