From 9f2c815e13dd6cde6bc6bb489a0bcc47e1864da3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:29:19 +1000 Subject: [PATCH] feat(ui): revise entity menus --- invokeai/frontend/web/public/locales/en.json | 7 +- .../components/AddLayerButton.tsx | 30 ++- .../components/AddPromptButtons.tsx | 19 +- .../components/ControlLayer/ControlLayer.tsx | 2 - .../ControlLayer/ControlLayerActionsMenu.tsx | 17 -- .../ControlLayerControlAdapter.tsx | 8 +- ...ControlLayerControlAdapterControlMode.tsx} | 4 +- .../ControlLayerControlAdapterModel.tsx} | 4 +- .../ControlLayer/ControlLayerMenuItems.tsx | 23 ++ .../ControlLayerMenuItemsControlToRaster.tsx | 25 +++ .../components/ControlLayersPanelContent.tsx | 4 +- .../components/InpaintMask/InpaintMask.tsx | 2 - .../InpaintMask/InpaintMaskActionsMenu.tsx | 17 -- .../InpaintMask/InpaintMaskMenuItems.tsx | 12 ++ .../components/RasterLayer/RasterLayer.tsx | 2 - .../RasterLayer/RasterLayerActionsMenu.tsx | 17 -- .../RasterLayer/RasterLayerMenuItems.tsx | 23 ++ .../RasterLayerMenuItemsRasterToControl.tsx | 28 +++ .../RegionalGuidance/RegionalGuidance.tsx | 2 - .../RegionalGuidanceActionsMenu.tsx | 62 ------ .../RegionalGuidanceMenuItems.tsx | 21 ++ ...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 58 +++++ ...sButton.tsx => ResetAllEntitiesButton.tsx} | 17 +- .../common/CanvasEntityActionMenuItems.tsx | 200 ------------------ .../components/common/CanvasEntityHeader.tsx | 52 ++++- .../common/CanvasEntityMenuItemsArrange.tsx | 95 +++++++++ .../common/CanvasEntityMenuItemsDelete.tsx | 25 +++ .../common/CanvasEntityMenuItemsFilter.tsx | 22 ++ .../common/CanvasEntityMenuItemsReset.tsx | 25 +++ .../controlLayers/hooks/addLayerHooks.ts | 89 -------- .../hooks/useLayerControlAdapter.ts | 24 ++- .../controlLayers/store/canvasV2Slice.ts | 12 +- 32 files changed, 475 insertions(+), 473 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAdapter/ControlAdapterControlModeSelect.tsx => ControlLayer/ControlLayerControlAdapterControlMode.tsx} (90%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAdapter/ControlAdapterModel.tsx => ControlLayer/ControlLayerControlAdapterModel.tsx} (91%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{DeleteAllLayersButton.tsx => ResetAllEntitiesButton.tsx} (56%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 42b7f80fc8..b137b45f1a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 0234f49ab3..433faf4c69 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -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 ( @@ -29,18 +35,10 @@ export const AddLayerButton = memo(() => { {t('controlLayers.addLayer')} - } onClick={addRGLayer}> - {t('controlLayers.regionalGuidanceLayer')} - - } onClick={addRasterLayer}> - {t('controlLayers.rasterLayer')} - - } onClick={addCALayer} isDisabled={isAddCALayerDisabled}> - {t('controlLayers.globalControlAdapterLayer')} - - } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> - {t('controlLayers.globalIPAdapterLayer')} - + {t('controlLayers.regionalGuidanceLayer')} + {t('controlLayers.rasterLayer')} + {t('controlLayers.controlLayer')} + {t('controlLayers.globalIPAdapterLayer')} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 7256d6d9e0..4b0804c982 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -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 ( @@ -58,13 +65,7 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => { > {t('common.negativePrompt')} - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx index 046d43f46e..af47649a20 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayer.tsx @@ -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) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx deleted file mode 100644 index 3f2fa45e06..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerActionsMenu.tsx +++ /dev/null @@ -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 ( - - - - - - - ); -}); - -ControlLayerActionsMenu.displayName = 'ControlLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx index 3e5d1d7fee..435e90cdd4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx @@ -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 ( - + {controlAdapter.type === 'controlnet' && ( - + )} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode.tsx similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode.tsx index e3f03b6d48..e0f57e6df8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx similarity index 91% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx index 13e4b17e90..f50218e4aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/ControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx new file mode 100644 index 0000000000..49f2558906 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx @@ -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 ( + <> + + + + + + + + + ); +}); + +ControlLayerMenuItems.displayName = 'ControlLayerMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx new file mode 100644 index 0000000000..e64df4e9e5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx @@ -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 ( + }> + {t('controlLayers.convertToRasterLayer')} + + ); +}); + +ControlLayerMenuItemsControlToRaster.displayName = 'ControlLayerMenuItemsControlToRaster'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index ddaa009055..195d774707 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -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(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx index 04b4f88ee6..ba3c3dee82 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMask.tsx @@ -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(() => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx deleted file mode 100644 index 5ce4024195..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskActionsMenu.tsx +++ /dev/null @@ -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 ( - - - - - - - ); -}); - -InpaintMaskActionsMenu.displayName = 'InpaintMaskActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx new file mode 100644 index 0000000000..d0d1a7dc10 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx @@ -0,0 +1,12 @@ +import { CanvasEntityMenuItemsReset } from 'features/controlLayers/components/common/CanvasEntityMenuItemsReset'; +import { memo } from 'react'; + +export const InpaintMaskMenuItems = memo(() => { + return ( + <> + + + ); +}); + +InpaintMaskMenuItems.displayName = 'InpaintMaskMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index e12ce65b4a..303d4191ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -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) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx deleted file mode 100644 index 576c939ad2..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerActionsMenu.tsx +++ /dev/null @@ -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 ( - - - - - - - ); -}); - -RasterLayerActionsMenu.displayName = 'RasterLayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx new file mode 100644 index 0000000000..2ad6a48f10 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx @@ -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 ( + <> + + + + + + + + + ); +}); + +RasterLayerMenuItems.displayName = 'RasterLayerMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx new file mode 100644 index 0000000000..9513f0409c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx @@ -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 ( + }> + {t('controlLayers.convertToControlLayer')} + + ); +}); + +RasterLayerMenuItemsRasterToControl.displayName = 'RasterLayerMenuItemsRasterToControl'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx index eeed5126a9..fd368e53e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidance.tsx @@ -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) => { - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx deleted file mode 100644 index e8d9db8885..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceActionsMenu.tsx +++ /dev/null @@ -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 ( - - - - }> - {t('controlLayers.addPositivePrompt')} - - }> - {t('controlLayers.addNegativePrompt')} - - } isDisabled={isAddIPAdapterDisabled}> - {t('controlLayers.addIPAdapter')} - - - - - - ); -}); - -RegionalGuidanceActionsMenu.displayName = 'RegionalGuidanceActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx new file mode 100644 index 0000000000..9c495bb6db --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx @@ -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 ( + <> + + + + + + + + ); +}); + +RegionalGuidanceMenuItems.displayName = 'RegionalGuidanceMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx new file mode 100644 index 0000000000..66bcee04e9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx @@ -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 ( + <> + + {t('controlLayers.addPositivePrompt')} + + + {t('controlLayers.addNegativePrompt')} + + {t('controlLayers.addIPAdapter')} + + ); +}); + +RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.displayName = 'RegionalGuidanceMenuItemsExtra'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx similarity index 56% rename from invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx index b6dc6b4df0..6851f7c5ab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ResetAllEntitiesButton.tsx @@ -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={} variant="ghost" colorScheme="error" - isDisabled={entityCount === 0} data-testid="control-layers-delete-all-layers-button" > - {t('controlLayers.deleteAll')} + {t('controlLayers.resetAll')} ); }); -DeleteAllLayersButton.displayName = 'DeleteAllLayersButton'; +ResetAllEntitiesButton.displayName = 'ResetAllEntitiesButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx deleted file mode 100644 index 1c8b61c01e..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx +++ /dev/null @@ -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 && ( - <> - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - - )} - {isFilterable && ( - }> - {t('common.filter')} - - )} - {isRasterLayer && ( - }> - {t('common.convertToControlLayer')} - - )} - {isControlLayer && ( - }> - {t('common.convertToRasterLayer')} - - )} - - }> - {t('accessibility.reset')} - - {isDeleteable && ( - } color="error.300"> - {t('common.delete')} - - )} - - } color="warn.300"> - {t('common.debug')} - - - ); -}); - -CanvasEntityActionMenuItems.displayName = 'CanvasEntityActionMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx index 5fe0203861..0521ab803d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx @@ -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 ( - - - - ); - }, []); + if (entityIdentifier.type === 'regional_guidance') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'inpaint_mask') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'raster_layer') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'control_layer') { + return ( + + + + ); + } + + if (entityIdentifier.type === 'ip_adapter') { + return {/* */}; + } + + assert(false, 'Unhandled entity type'); + }, [entityIdentifier]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx new file mode 100644 index 0000000000..8bcb88f623 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -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 ( + <> + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + + ); +}); + +CanvasEntityMenuItemsArrange.displayName = 'CanvasEntityArrangeMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx new file mode 100644 index 0000000000..24d9a32682 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDelete.tsx @@ -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 ( + } color="error.300"> + {t('common.delete')} + + ); +}); + +CanvasEntityMenuItemsDelete.displayName = 'CanvasEntityMenuItemsDelete'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx new file mode 100644 index 0000000000..5e15543e5c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx @@ -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 ( + }> + {t('controlLayers.filter')} + + ); +}); + +CanvasEntityMenuItemsFilter.displayName = 'CanvasEntityMenuItemsFilter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx new file mode 100644 index 0000000000..3a2387bc86 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsReset.tsx @@ -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 ( + }> + {t('accessibility.reset')} + + ); +}); + +CanvasEntityMenuItemsReset.displayName = 'CanvasEntityMenuItemsReset'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts deleted file mode 100644 index c7fd177d75..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ /dev/null @@ -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; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts index 9c91bca847..cc174e8771 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts @@ -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; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index cc70237b85..c3818f3703 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -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);