feat(ui): revise entity menus

This commit is contained in:
psychedelicious 2024-08-15 11:29:19 +10:00
parent 1435557d1d
commit 9f2c815e13
32 changed files with 475 additions and 473 deletions

View File

@ -1646,7 +1646,7 @@
"storeNotInitialized": "Store is not initialized"
},
"controlLayers": {
"deleteAll": "Delete All",
"resetAll": "Reset All",
"addLayer": "Add Layer",
"moveToFront": "Move to Front",
"moveToBack": "Move to Back",
@ -1694,7 +1694,10 @@
"layers_other": "Layers",
"objects_zero": "empty",
"objects_one": "{{count}} object",
"objects_other": "{{count}} objects"
"objects_other": "{{count}} objects",
"filter": "Filter",
"convertToControlLayer": "Convert to Control Layer",
"convertToRasterLayer": "Convert to Raster Layer"
},
"upscaling": {
"upscale": "Upscale",

View File

@ -1,7 +1,7 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice';
import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import { controlLayerAdded, ipaAdded, rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
@ -9,14 +9,20 @@ import { PiPlusBold } from 'react-icons/pi';
export const AddLayerButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [addCALayer, isAddCALayerDisabled] = useAddCALayer();
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
const defaultControlAdapter = useDefaultControlAdapter();
const defaultIPAdapter = useDefaultIPAdapter();
const addRGLayer = useCallback(() => {
dispatch(rgAdded());
}, [dispatch]);
const addRasterLayer = useCallback(() => {
dispatch(rasterLayerAdded({ isSelected: true }));
}, [dispatch]);
const addControlLayer = useCallback(() => {
dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } }));
}, [defaultControlAdapter, dispatch]);
const addIPAdapter = useCallback(() => {
dispatch(ipaAdded({ config: defaultIPAdapter }));
}, [defaultIPAdapter, dispatch]);
return (
<Menu>
@ -29,18 +35,10 @@ export const AddLayerButton = memo(() => {
{t('controlLayers.addLayer')}
</MenuButton>
<MenuList>
<MenuItem icon={<PiPlusBold />} onClick={addRGLayer}>
{t('controlLayers.regionalGuidanceLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addCALayer} isDisabled={isAddCALayerDisabled}>
{t('controlLayers.globalControlAdapterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}>
{t('controlLayers.globalIPAdapterLayer')}
</MenuItem>
<MenuItem onClick={addRGLayer}>{t('controlLayers.regionalGuidanceLayer')}</MenuItem>
<MenuItem onClick={addRasterLayer}>{t('controlLayers.rasterLayer')}</MenuItem>
<MenuItem onClick={addControlLayer}>{t('controlLayers.controlLayer')}</MenuItem>
<MenuItem onClick={addIPAdapter}>{t('controlLayers.globalIPAdapterLayer')}</MenuItem>
</MenuList>
</Menu>
);

View File

@ -1,8 +1,10 @@
import { Button, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import { nanoid } from 'features/controlLayers/konva/util';
import {
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
selectCanvasV2Slice,
@ -18,7 +20,7 @@ type AddPromptButtonProps = {
export const AddPromptButtons = ({ id }: AddPromptButtonProps) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id);
const defaultIPAdapter = useDefaultIPAdapter();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
@ -37,6 +39,11 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => {
const addNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id, prompt: '' }));
}, [dispatch, id]);
const addIPAdapter = useCallback(() => {
dispatch(
rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid(), type: 'ip_adapter', isEnabled: true } })
);
}, [defaultIPAdapter, dispatch, id]);
return (
<Flex w="full" p={2} justifyContent="space-between">
@ -58,13 +65,7 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => {
>
{t('common.negativePrompt')}
</Button>
<Button
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addIPAdapter}
isDisabled={isAddIPAdapterDisabled}
>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
{t('common.ipAdapter')}
</Button>
</Flex>

View File

@ -5,7 +5,6 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { ControlLayerActionsMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerActionsMenu';
import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
@ -25,7 +24,6 @@ export const ControlLayer = memo(({ id }: Props) => {
<CanvasEntityEnabledToggle />
<CanvasEntityTitle />
<Spacer />
<ControlLayerActionsMenu />
<CanvasEntityDeleteButton />
</CanvasEntityHeader>
<CanvasEntitySettingsWrapper>

View File

@ -1,17 +0,0 @@
import { Menu, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { memo } from 'react';
export const ControlLayerActionsMenu = memo(() => {
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<CanvasEntityActionMenuItems />
</MenuList>
</Menu>
);
});
ControlLayerActionsMenu.displayName = 'ControlLayerActionsMenu';

View File

@ -2,8 +2,8 @@ import { Flex } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect';
import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel';
import { ControlLayerControlAdapterControlMode } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode';
import { ControlLayerControlAdapterModel } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useControlLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import {
@ -51,11 +51,11 @@ export const ControlLayerControlAdapter = memo(() => {
return (
<Flex flexDir="column" gap={3} position="relative" w="full">
<ControlAdapterModel modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
<ControlLayerControlAdapterModel modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
<Weight weight={controlAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct beginEndStepPct={controlAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
{controlAdapter.type === 'controlnet' && (
<ControlAdapterControlModeSelect controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
<ControlLayerControlAdapterControlMode controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
)}
</Flex>
);

View File

@ -12,7 +12,7 @@ type Props = {
onChange: (controlMode: ControlModeV2) => void;
};
export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => {
export const ControlLayerControlAdapterControlMode = memo(({ controlMode, onChange }: Props) => {
const { t } = useTranslation();
const CONTROL_MODE_DATA = useMemo(
() => [
@ -57,4 +57,4 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }:
);
});
ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect';
ControlLayerControlAdapterControlMode.displayName = 'ControlLayerControlAdapterControlMode';

View File

@ -11,7 +11,7 @@ type Props = {
onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void;
};
export const ControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => {
export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => {
const { t } = useTranslation();
const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base);
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
@ -60,4 +60,4 @@ export const ControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }:
);
});
ControlAdapterModel.displayName = 'ControlAdapterModel';
ControlLayerControlAdapterModel.displayName = 'ControlLayerControlAdapterModel';

View File

@ -0,0 +1,23 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset';
import { ControlLayerMenuItemsControlToRaster } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster';
import { memo } from 'react';
export const ControlLayerMenuItems = memo(() => {
return (
<>
<CanvasEntityMenuItemsFilter />
<ControlLayerMenuItemsControlToRaster />
<MenuDivider />
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsReset />
<CanvasEntityMenuItemsDelete />
</>
);
});
ControlLayerMenuItems.displayName = 'ControlLayerMenuItems';

View File

@ -0,0 +1,25 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLightningBold } from 'react-icons/pi';
export const ControlLayerMenuItemsControlToRaster = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const convertControlLayerToRasterLayer = useCallback(() => {
dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id }));
}, [dispatch, entityIdentifier.id]);
return (
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />}>
{t('controlLayers.convertToRasterLayer')}
</MenuItem>
);
});
ControlLayerMenuItemsControlToRaster.displayName = 'ControlLayerMenuItemsControlToRaster';

View File

@ -3,8 +3,8 @@ import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
import { Filter } from 'features/controlLayers/components/Filters/Filter';
import { ResetAllEntitiesButton } from 'features/controlLayers/components/ResetAllEntitiesButton';
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { memo } from 'react';
@ -18,7 +18,7 @@ export const ControlLayersPanelContent = memo(() => {
<Flex flexDir="column" gap={2} w="full" h="full">
<Flex justifyContent="space-around">
<AddLayerButton />
<DeleteAllLayersButton />
<ResetAllEntitiesButton />
</Flex>
<CanvasEntityList />
</Flex>

View File

@ -5,7 +5,6 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com
import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { InpaintMaskActionsMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
@ -28,7 +27,6 @@ export const InpaintMask = memo(() => {
<CanvasEntityTitle />
<Spacer />
<InpaintMaskMaskFillColorPicker />
<InpaintMaskActionsMenu />
</CanvasEntityHeader>
</CanvasEntityContainer>
</EntityIdentifierContext.Provider>

View File

@ -1,17 +0,0 @@
import { Menu, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { memo } from 'react';
export const InpaintMaskActionsMenu = memo(() => {
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<CanvasEntityActionMenuItems />
</MenuList>
</Menu>
);
});
InpaintMaskActionsMenu.displayName = 'InpaintMaskActionsMenu';

View File

@ -0,0 +1,12 @@
import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset';
import { memo } from 'react';
export const InpaintMaskMenuItems = memo(() => {
return (
<>
<CanvasEntityMenuItemsReset />
</>
);
});
InpaintMaskMenuItems.displayName = 'InpaintMaskMenuItems';

View File

@ -4,7 +4,6 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { RasterLayerActionsMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerActionsMenu';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
@ -23,7 +22,6 @@ export const RasterLayer = memo(({ id }: Props) => {
<CanvasEntityEnabledToggle />
<CanvasEntityTitle />
<Spacer />
<RasterLayerActionsMenu />
<CanvasEntityDeleteButton />
</CanvasEntityHeader>
</CanvasEntityContainer>

View File

@ -1,17 +0,0 @@
import { Menu, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { memo } from 'react';
export const RasterLayerActionsMenu = memo(() => {
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<CanvasEntityActionMenuItems />
</MenuList>
</Menu>
);
});
RasterLayerActionsMenu.displayName = 'RasterLayerActionsMenu';

View File

@ -0,0 +1,23 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset';
import { RasterLayerMenuItemsRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl';
import { memo } from 'react';
export const RasterLayerMenuItems = memo(() => {
return (
<>
<CanvasEntityMenuItemsFilter />
<RasterLayerMenuItemsRasterToControl />
<MenuDivider />
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsReset />
<CanvasEntityMenuItemsDelete />
</>
);
});
RasterLayerMenuItems.displayName = 'RasterLayerMenuItems';

View File

@ -0,0 +1,28 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useDefaultControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLightningBold } from 'react-icons/pi';
export const RasterLayerMenuItemsRasterToControl = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const defaultControlAdapter = useDefaultControlAdapter();
const convertRasterLayerToControlLayer = useCallback(() => {
dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id, controlAdapter: defaultControlAdapter }));
}, [dispatch, defaultControlAdapter, entityIdentifier.id]);
return (
<MenuItem onClick={convertRasterLayerToControlLayer} icon={<PiLightningBold />}>
{t('controlLayers.convertToControlLayer')}
</MenuItem>
);
});
RasterLayerMenuItemsRasterToControl.displayName = 'RasterLayerMenuItemsRasterToControl';

View File

@ -4,7 +4,6 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { RegionalGuidanceActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu';
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
@ -30,7 +29,6 @@ export const RegionalGuidance = memo(({ id }: Props) => {
<RegionalGuidanceBadges />
<RegionalGuidanceMaskFillColorPicker />
<RegionalGuidanceSettingsPopover />
<RegionalGuidanceActionsMenu />
<CanvasEntityDeleteButton />
</CanvasEntityHeader>
<RegionalGuidanceSettings />

View File

@ -1,62 +0,0 @@
import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
rgNegativePromptChanged,
rgPositivePromptChanged,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const RegionalGuidanceActionsMenu = memo(() => {
const { id } = useEntityIdentifierContext();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id);
const selectActionsValidity = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rg = selectRGOrThrow(canvasV2, id);
return {
isAddPositivePromptDisabled: rg.positivePrompt === null,
isAddNegativePromptDisabled: rg.negativePrompt === null,
};
}),
[id]
);
const actions = useAppSelector(selectActionsValidity);
const onAddPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]);
const onAddNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]);
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<MenuItem onClick={onAddPositivePrompt} isDisabled={actions.isAddPositivePromptDisabled} icon={<PiPlusBold />}>
{t('controlLayers.addPositivePrompt')}
</MenuItem>
<MenuItem onClick={onAddNegativePrompt} isDisabled={actions.isAddNegativePromptDisabled} icon={<PiPlusBold />}>
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={onAddIPAdapter} icon={<PiPlusBold />} isDisabled={isAddIPAdapterDisabled}>
{t('controlLayers.addIPAdapter')}
</MenuItem>
<MenuDivider />
<CanvasEntityActionMenuItems />
</MenuList>
</Menu>
);
});
RegionalGuidanceActionsMenu.displayName = 'RegionalGuidanceActionsMenu';

View File

@ -0,0 +1,21 @@
import { MenuDivider } from '@invoke-ai/ui-library';
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset';
import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter';
import { memo } from 'react';
export const RegionalGuidanceMenuItems = memo(() => {
return (
<>
<RegionalGuidanceMenuItemsAddPromptsAndIPAdapter />
<MenuDivider />
<CanvasEntityMenuItemsArrange />
<MenuDivider />
<CanvasEntityMenuItemsReset />
<CanvasEntityMenuItemsDelete />
</>
);
});
RegionalGuidanceMenuItems.displayName = 'RegionalGuidanceMenuItems';

View File

@ -0,0 +1,58 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import { nanoid } from 'features/controlLayers/konva/util';
import {
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
const { id } = useEntityIdentifierContext();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const defaultIPAdapter = useDefaultIPAdapter();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rg = canvasV2.regions.entities.find((rg) => rg.id === id);
return {
canAddPositivePrompt: rg?.positivePrompt === null,
canAddNegativePrompt: rg?.negativePrompt === null,
};
}),
[id]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]);
const addNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]);
const addIPAdapter = useCallback(() => {
dispatch(
rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid(), type: 'ip_adapter', isEnabled: true } })
);
}, [defaultIPAdapter, dispatch, id]);
return (
<>
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt}>
{t('controlLayers.addPositivePrompt')}
</MenuItem>
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt}>
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={addIPAdapter}>{t('controlLayers.addIPAdapter')}</MenuItem>
</>
);
});
RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.displayName = 'RegionalGuidanceMenuItemsExtra';

View File

@ -1,21 +1,13 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppDispatch } from 'app/store/storeHooks';
import { allEntitiesDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
export const DeleteAllLayersButton = memo(() => {
export const ResetAllEntitiesButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityCount = useAppSelector((s) => {
return (
s.canvasV2.regions.entities.length +
// s.canvasV2.controlAdapters.entities.length +
s.canvasV2.ipAdapters.entities.length +
s.canvasV2.rasterLayers.entities.length
);
});
const onClick = useCallback(() => {
dispatch(allEntitiesDeleted());
}, [dispatch]);
@ -26,12 +18,11 @@ export const DeleteAllLayersButton = memo(() => {
leftIcon={<PiTrashSimpleBold />}
variant="ghost"
colorScheme="error"
isDisabled={entityCount === 0}
data-testid="control-layers-delete-all-layers-button"
>
{t('controlLayers.deleteAll')}
{t('controlLayers.resetAll')}
</Button>
);
});
DeleteAllLayersButton.displayName = 'DeleteAllLayersButton';
ResetAllEntitiesButton.displayName = 'ResetAllEntitiesButton';

View File

@ -1,200 +0,0 @@
import { MenuDivider, MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useDefaultControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import {
$filteringEntity,
controlLayerConvertedToRasterLayer,
entityArrangedBackwardOne,
entityArrangedForwardOne,
entityArrangedToBack,
entityArrangedToFront,
entityDeleted,
entityReset,
rasterLayerConvertedToControlLayer,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowCounterClockwiseBold,
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiCheckBold,
PiQuestionMarkBold,
PiStarHalfBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
const getIndexAndCount = (
canvasV2: CanvasV2State,
{ id, type }: CanvasEntityIdentifier
): { index: number; count: number } => {
if (type === 'raster_layer') {
return {
index: canvasV2.rasterLayers.entities.findIndex((entity) => entity.id === id),
count: canvasV2.rasterLayers.entities.length,
};
} else if (type === 'control_layer') {
return {
index: canvasV2.controlLayers.entities.findIndex((entity) => entity.id === id),
count: canvasV2.controlLayers.entities.length,
};
} else if (type === 'regional_guidance') {
return {
index: canvasV2.regions.entities.findIndex((entity) => entity.id === id),
count: canvasV2.regions.entities.length,
};
} else {
return {
index: -1,
count: 0,
};
}
};
export const CanvasEntityActionMenuItems = memo(() => {
const { t } = useTranslation();
const canvasManager = useStore($canvasManager);
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const { index, count } = getIndexAndCount(canvasV2, entityIdentifier);
return {
canMoveForwardOne: index < count - 1,
canMoveBackwardOne: index > 0,
canMoveToFront: index < count - 1,
canMoveToBack: index > 0,
};
}),
[entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const isArrangeable = useMemo(
() =>
entityIdentifier.type === 'raster_layer' ||
entityIdentifier.type === 'control_layer' ||
entityIdentifier.type === 'regional_guidance',
[entityIdentifier.type]
);
const isDeleteable = useMemo(
() =>
entityIdentifier.type === 'raster_layer' ||
entityIdentifier.type === 'control_layer' ||
entityIdentifier.type === 'regional_guidance',
[entityIdentifier.type]
);
const isFilterable = useMemo(
() => entityIdentifier.type === 'raster_layer' || entityIdentifier.type === 'control_layer',
[entityIdentifier.type]
);
const isRasterLayer = useMemo(() => entityIdentifier.type === 'raster_layer', [entityIdentifier.type]);
const isControlLayer = useMemo(() => entityIdentifier.type === 'control_layer', [entityIdentifier.type]);
const defaultControlAdapter = useDefaultControlAdapter();
const convertRasterLayerToControlLayer = useCallback(() => {
dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id, controlAdapter: defaultControlAdapter }));
}, [dispatch, defaultControlAdapter, entityIdentifier.id]);
const convertControlLayerToRasterLayer = useCallback(() => {
dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id }));
}, [dispatch, entityIdentifier.id]);
const deleteEntity = useCallback(() => {
dispatch(entityDeleted({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const resetEntity = useCallback(() => {
dispatch(entityReset({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveForwardOne = useCallback(() => {
dispatch(entityArrangedForwardOne({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveToFront = useCallback(() => {
dispatch(entityArrangedToFront({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveBackwardOne = useCallback(() => {
dispatch(entityArrangedBackwardOne({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveToBack = useCallback(() => {
dispatch(entityArrangedToBack({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const filter = useCallback(() => {
$filteringEntity.set(entityIdentifier);
}, [entityIdentifier]);
const debug = useCallback(() => {
if (!canvasManager) {
return;
}
const entity = canvasManager.stateApi.getEntity(entityIdentifier);
if (!entity) {
return;
}
console.debug(entity);
}, [canvasManager, entityIdentifier]);
return (
<>
{isArrangeable && (
<>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForwardOne} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackwardOne} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
</>
)}
{isFilterable && (
<MenuItem onClick={filter} icon={<PiStarHalfBold />}>
{t('common.filter')}
</MenuItem>
)}
{isRasterLayer && (
<MenuItem onClick={convertRasterLayerToControlLayer} icon={<PiCheckBold />}>
{t('common.convertToControlLayer')}
</MenuItem>
)}
{isControlLayer && (
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiCheckBold />}>
{t('common.convertToRasterLayer')}
</MenuItem>
)}
<MenuDivider />
<MenuItem onClick={resetEntity} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>
{isDeleteable && (
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
)}
<MenuDivider />
<MenuItem onClick={debug} icon={<PiQuestionMarkBold />} color="warn.300">
{t('common.debug')}
</MenuItem>
</>
);
});
CanvasEntityActionMenuItems.displayName = 'CanvasEntityActionMenuItems';

View File

@ -1,16 +1,54 @@
import type { FlexProps } from '@invoke-ai/ui-library';
import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems';
import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo, useCallback } from 'react';
import { assert } from 'tsafe';
export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => {
const entityIdentifier = useEntityIdentifierContext();
const renderMenu = useCallback(() => {
return (
<MenuList>
<CanvasEntityActionMenuItems />
</MenuList>
);
}, []);
if (entityIdentifier.type === 'regional_guidance') {
return (
<MenuList>
<RegionalGuidanceMenuItems />
</MenuList>
);
}
if (entityIdentifier.type === 'inpaint_mask') {
return (
<MenuList>
<InpaintMaskMenuItems />
</MenuList>
);
}
if (entityIdentifier.type === 'raster_layer') {
return (
<MenuList>
<RasterLayerMenuItems />
</MenuList>
);
}
if (entityIdentifier.type === 'control_layer') {
return (
<MenuList>
<ControlLayerMenuItems />
</MenuList>
);
}
if (entityIdentifier.type === 'ip_adapter') {
return <MenuList>{/* <ControlLayerMenuItems /> */}</MenuList>;
}
assert(false, 'Unhandled entity type');
}, [entityIdentifier]);
return (
<ContextMenu renderMenu={renderMenu}>

View File

@ -0,0 +1,95 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
entityArrangedBackwardOne,
entityArrangedForwardOne,
entityArrangedToBack,
entityArrangedToFront,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi';
const getIndexAndCount = (
canvasV2: CanvasV2State,
{ id, type }: CanvasEntityIdentifier
): { index: number; count: number } => {
if (type === 'raster_layer') {
return {
index: canvasV2.rasterLayers.entities.findIndex((entity) => entity.id === id),
count: canvasV2.rasterLayers.entities.length,
};
} else if (type === 'control_layer') {
return {
index: canvasV2.controlLayers.entities.findIndex((entity) => entity.id === id),
count: canvasV2.controlLayers.entities.length,
};
} else if (type === 'regional_guidance') {
return {
index: canvasV2.regions.entities.findIndex((entity) => entity.id === id),
count: canvasV2.regions.entities.length,
};
} else {
return {
index: -1,
count: 0,
};
}
};
export const CanvasEntityMenuItemsArrange = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const { index, count } = getIndexAndCount(canvasV2, entityIdentifier);
return {
canMoveForwardOne: index < count - 1,
canMoveBackwardOne: index > 0,
canMoveToFront: index < count - 1,
canMoveToBack: index > 0,
};
}),
[entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const moveForwardOne = useCallback(() => {
dispatch(entityArrangedForwardOne({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveToFront = useCallback(() => {
dispatch(entityArrangedToFront({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveBackwardOne = useCallback(() => {
dispatch(entityArrangedBackwardOne({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
const moveToBack = useCallback(() => {
dispatch(entityArrangedToBack({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForwardOne} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackwardOne} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
</>
);
});
CanvasEntityMenuItemsArrange.displayName = 'CanvasEntityArrangeMenuItems';

View File

@ -0,0 +1,25 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
export const CanvasEntityMenuItemsDelete = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const deleteEntity = useCallback(() => {
dispatch(entityDeleted({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
);
});
CanvasEntityMenuItemsDelete.displayName = 'CanvasEntityMenuItemsDelete';

View File

@ -0,0 +1,22 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShootingStarBold } from 'react-icons/pi';
export const CanvasEntityMenuItemsFilter = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const filter = useCallback(() => {
$filteringEntity.set(entityIdentifier);
}, [entityIdentifier]);
return (
<MenuItem onClick={filter} icon={<PiShootingStarBold />}>
{t('controlLayers.filter')}
</MenuItem>
);
});
CanvasEntityMenuItemsFilter.displayName = 'CanvasEntityMenuItemsFilter';

View File

@ -0,0 +1,25 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { entityReset } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
export const CanvasEntityMenuItemsReset = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const resetEntity = useCallback(() => {
dispatch(entityReset({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={resetEntity} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>
);
});
CanvasEntityMenuItemsReset.displayName = 'CanvasEntityMenuItemsReset';

View File

@ -1,89 +0,0 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import { ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice';
import {
IMAGE_FILTERS,
initialControlNetV2,
initialIPAdapterV2,
initialT2IAdapterV2,
isFilterType,
} from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback, useMemo } from 'react';
import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType';
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { v4 as uuidv4 } from 'uuid';
export const useAddCALayer = () => {
const dispatch = useAppDispatch();
const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base);
const [modelConfigs] = useControlNetAndT2IAdapterModels();
const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => {
// prefer to use a model that matches the base model
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
return compatibleModels[0] ?? modelConfigs[0] ?? null;
}, [baseModel, modelConfigs]);
const isDisabled = useMemo(() => !model, [model]);
const addCALayer = useCallback(() => {
if (!model) {
return;
}
const defaultPreprocessor = model.default_settings?.preprocessor;
const processorConfig = isFilterType(defaultPreprocessor)
? IMAGE_FILTERS[defaultPreprocessor].buildDefaults(baseModel)
: null;
const initialConfig = deepClone(model.type === 'controlnet' ? initialControlNetV2 : initialT2IAdapterV2);
const config = { ...initialConfig, model: zModelIdentifierField.parse(model), processorConfig };
// dispatch(caAdded({ config }));
}, [dispatch, model, baseModel]);
return [addCALayer, isDisabled] as const;
};
export const useAddIPALayer = () => {
const dispatch = useAppDispatch();
const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base);
const [modelConfigs] = useIPAdapterModels();
const model: IPAdapterModelConfig | null = useMemo(() => {
// prefer to use a model that matches the base model
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
return compatibleModels[0] ?? modelConfigs[0] ?? null;
}, [baseModel, modelConfigs]);
const isDisabled = useMemo(() => !model, [model]);
const addIPALayer = useCallback(() => {
if (!model) {
return;
}
const initialConfig = deepClone(initialIPAdapterV2);
const config = { ...initialConfig, model: zModelIdentifierField.parse(model) };
dispatch(ipaAdded({ config }));
}, [dispatch, model]);
return [addIPALayer, isDisabled] as const;
};
export const useAddIPAdapterToRGLayer = (id: string) => {
const dispatch = useAppDispatch();
const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base);
const [modelConfigs] = useIPAdapterModels();
const model: IPAdapterModelConfig | null = useMemo(() => {
// prefer to use a model that matches the base model
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
return compatibleModels[0] ?? modelConfigs[0] ?? null;
}, [baseModel, modelConfigs]);
const isDisabled = useMemo(() => !model, [model]);
const addIPAdapter = useCallback(() => {
if (!model) {
return;
}
const initialConfig = deepClone(initialIPAdapterV2);
const config = { ...initialConfig, model: zModelIdentifierField.parse(model) };
dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...config, id: uuidv4(), type: 'ip_adapter', isEnabled: true } }));
}, [model, dispatch, id]);
return [addIPAdapter, isDisabled] as const;
};

View File

@ -4,10 +4,10 @@ import { deepClone } from 'common/util/deepClone';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectControlLayerOrThrow } from 'features/controlLayers/store/controlLayersReducers';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { initialControlNetV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types';
import { initialControlNetV2, initialIPAdapterV2, initialT2IAdapterV2 } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { useMemo } from 'react';
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType';
export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => {
const selectControlAdapter = useMemo(
@ -42,3 +42,23 @@ export const useDefaultControlAdapter = () => {
return defaultControlAdapter;
};
export const useDefaultIPAdapter = () => {
const [modelConfigs] = useIPAdapterModels();
const baseModel = useAppSelector((s) => s.canvasV2.params.model?.base);
const defaultControlAdapter = useMemo(() => {
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
const model = compatibleModels[0] ?? modelConfigs[0] ?? null;
const ipAdapter = deepClone(initialIPAdapterV2);
if (model) {
ipAdapter.model = zModelIdentifierField.parse(model);
}
return ipAdapter;
}, [baseModel, modelConfigs]);
return defaultControlAdapter;
};

View File

@ -44,7 +44,7 @@ import { IMAGE_FILTERS, isDrawableEntity, RGBA_RED } from './types';
const initialState: CanvasV2State = {
_version: 3,
selectedEntityIdentifier: null,
selectedEntityIdentifier: { id: 'inpaint_mask', type: 'inpaint_mask' },
rasterLayers: { entities: [], compositeRasterizationCache: [] },
controlLayers: { entities: [] },
ipAdapters: { entities: [] },
@ -385,10 +385,13 @@ export const canvasV2Slice = createSlice({
}
},
allEntitiesDeleted: (state) => {
state.regions.entities = [];
state.rasterLayers.entities = [];
state.ipAdapters = deepClone(initialState.ipAdapters);
state.rasterLayers = deepClone(initialState.rasterLayers);
state.rasterLayers.compositeRasterizationCache = [];
state.ipAdapters.entities = [];
state.controlLayers = deepClone(initialState.controlLayers);
state.regions = deepClone(initialState.regions);
state.inpaintMask = deepClone(initialState.inpaintMask);
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
},
filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => {
state.filter.config = IMAGE_FILTERS[action.payload.type].buildDefaults();
@ -423,6 +426,7 @@ export const canvasV2Slice = createSlice({
state.ipAdapters = deepClone(initialState.ipAdapters);
state.rasterLayers = deepClone(initialState.rasterLayers);
state.rasterLayers.compositeRasterizationCache = [];
state.controlLayers = deepClone(initialState.controlLayers);
state.regions = deepClone(initialState.regions);
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);